Commit 17388eb0 authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into fix-cancelling-pipelines

* upstream/master: (133 commits)
  Restructure steps for MM slash commands service
  Add Changelog entry for CI linter validation fix
  Fix entry lookup in CI config inheritance rules
  Extend specs for global ci configuration entry
  Remove unnecessary require_relative calls from service classes
  Use single quote for strings
  Ue svg from SVGs object
  Dont trigger CI builds [ci skip]
  Revert "Test only migrations"
  Add custom copy for each empty stage
  Refactor Mattermost slash commands docs
  Fetch only one revision
  Highlight nav item on hover
  Test only migrations
  Fix migration paths tests
  Scroll CA stage panel on mobile
  Fix CSS declaration
  administer to administrator
  Move SVGs to JS objects for easy reuse
  Improve deploy command message
  ...
parents c7c4850d acaa6d73
...@@ -23,7 +23,9 @@ ...@@ -23,7 +23,9 @@
"spyOn": false, "spyOn": false,
"spyOnEvent": false, "spyOnEvent": false,
"Turbolinks": false, "Turbolinks": false,
"window": false "window": false,
"Vue": false,
"Flash": false,
"Cookies": false
} }
} }
...@@ -271,12 +271,17 @@ rake db:seed_fu: ...@@ -271,12 +271,17 @@ rake db:seed_fu:
- log/development.log - log/development.log
teaspoon: teaspoon:
cache:
paths:
- vendor/ruby
- node_modules/
stage: test stage: test
<<: *use-db <<: *use-db
script: script:
- curl --silent --location https://deb.nodesource.com/setup_6.x | bash - - curl --silent --location https://deb.nodesource.com/setup_6.x | bash -
- apt-get install --assume-yes nodejs - apt-get install --assume-yes nodejs
- npm install --global istanbul - npm install
- npm link istanbul
- rake teaspoon - rake teaspoon
artifacts: artifacts:
name: coverage-javascript name: coverage-javascript
...@@ -319,12 +324,11 @@ migration paths: ...@@ -319,12 +324,11 @@ migration paths:
- master@gitlab/gitlabhq - master@gitlab/gitlabhq
- master@gitlab/gitlab-ee - master@gitlab/gitlab-ee
script: script:
- git checkout HEAD . - git fetch origin v8.5.9
- git fetch --tags - git checkout -f FETCH_HEAD
- git checkout v8.5.9
- 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
- bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" --retry=3 - bundle install --without postgres production --jobs $(nproc) ${FLAGS[@]} --retry=3
- rake db:drop db:create db:schema:load db:seed_fu - rake db:drop db:create db:schema:load db:seed_fu
- git checkout $CI_BUILD_REF - git checkout $CI_BUILD_REF
- source scripts/prepare_build.sh - source scripts/prepare_build.sh
...@@ -346,8 +350,11 @@ coverage: ...@@ -346,8 +350,11 @@ coverage:
- coverage/assets/ - coverage/assets/
lint-javascript: lint-javascript:
cache:
paths:
- node_modules/
stage: test stage: test
image: "node:latest" image: "node:7.1"
before_script: before_script:
- npm install - npm install
script: script:
......
...@@ -32,6 +32,7 @@ entry. ...@@ -32,6 +32,7 @@ entry.
- Fix sidekiq stats in admin area (blackst0ne) - Fix sidekiq stats in admin area (blackst0ne)
- Added label description as tooltip to issue board list title - Added label description as tooltip to issue board list title
- Created cycle analytics bundle JavaScript file - Created cycle analytics bundle JavaScript file
- Make the milestone page more responsive (yury-n)
- Hides container registry when repository is disabled - Hides container registry when repository is disabled
- API: Fix booleans not recognized as such when using the `to_boolean` helper - API: Fix booleans not recognized as such when using the `to_boolean` helper
- Removed delete branch tooltip !6954 - Removed delete branch tooltip !6954
......
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-undef, quotes, no-var, padded-blocks, max-len */
(function() {
this.Activities = (function() {
function Activities() {
Pager.init(20, true, false, this.updateTooltips);
$(".event-filter-link").on("click", (function(_this) {
return function(event) {
event.preventDefault();
_this.toggleFilter($(event.currentTarget));
return _this.reloadActivities();
};
})(this));
}
Activities.prototype.updateTooltips = function() {
gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
};
Activities.prototype.reloadActivities = function() {
$(".content_list").html('');
Pager.init(20, true, false, this.updateTooltips);
};
Activities.prototype.toggleFilter = function(sender) {
var filter = sender.attr("id").split("_")[0];
$('.event-filter .active').removeClass("active");
Cookies.set("event_filter", filter);
sender.closest('li').toggleClass("active");
};
return Activities;
})();
}).call(this);
/* eslint-disable no-param-reassign, class-methods-use-this */
/* global Pager, Cookies */
((global) => {
class Activities {
constructor() {
Pager.init(20, true, false, this.updateTooltips);
$('.event-filter-link').on('click', (e) => {
e.preventDefault();
this.toggleFilter(e.currentTarget);
this.reloadActivities();
});
}
updateTooltips() {
gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
}
reloadActivities() {
$('.content_list').html('');
Pager.init(20, true, false, this.updateTooltips);
}
toggleFilter(sender) {
const $sender = $(sender);
const filter = $sender.attr('id').split('_')[0];
$('.event-filter .active').removeClass('active');
Cookies.set('event_filter', filter);
$sender.closest('li').toggleClass('active');
}
}
global.Activities = Activities;
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageCodeComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
<div class="item-details">
<img class="avatar" :src="mergeRequest.author.avatarUrl">
<h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url">
{{ mergeRequest.title }}
</a>
</h5>
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot;
<span>
Opened
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span>
<span>
by
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span>
</div>
<div class="item-time">
<total-time :time="mergeRequest.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageIssueComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
<div class="item-details">
<img class="avatar" :src="issue.author.avatarUrl">
<h5 class="item-title issue-title">
<a class="issue-title" :href="issue.url">
{{ issue.title }}
</a>
</h5>
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
&middot;
<span>
Opened
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span>
<span>
by
<a :href="issue.author.webUrl" class="issue-author-link">
{{ issue.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="issue.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StagePlanComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="commit in items" class="stage-event-item">
<div class="item-details item-conmmit-component">
<img class="avatar" :src="commit.author.avatarUrl">
<h5 class="item-title commit-title">
<a :href="commit.commitUrl">
{{ commit.title }}
</a>
</h5>
<span>
First
<span class="commit-icon">${global.cycleAnalytics.svgs.iconCommit}</span>
<a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a>
pushed by
<a :href="commit.author.webUrl" class="commit-author-link">
{{ commit.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="commit.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageProductionComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
<div class="item-details">
<img class="avatar" :src="issue.author.avatarUrl">
<h5 class="item-title issue-title">
<a class="issue-title" :href="issue.url">
{{ issue.title }}
</a>
</h5>
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
&middot;
<span>
Opened
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span>
<span>
by
<a :href="issue.author.webUrl" class="issue-author-link">
{{ issue.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="issue.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageReviewComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
<div class="item-details">
<img class="avatar" :src="mergeRequest.author.avatarUrl">
<h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url">
{{ mergeRequest.title }}
</a>
</h5>
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot;
<span>
Opened
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span>
<span>
by
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span>
<template v-if="mergeRequest.state === 'closed'">
<span class="merge-request-state">
<i class="fa fa-ban"></i>
{{ mergeRequest.state.toUpperCase() }}
</span>
</template>
<template v-else>
<span class="merge-request-branch" v-if="mergeRequest.branch">
<i class= "fa fa-code-fork"></i>
<a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a>
</span>
</template>
</div>
<div class="item-time">
<total-time :time="mergeRequest.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageStagingComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component">
<div class="item-details">
<img class="avatar" :src="build.author.avatarUrl">
<h5 class="item-title">
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i>
<a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
<span class="icon-branch">${global.cycleAnalytics.svgs.iconBranch}</span>
<a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
</h5>
<span>
<a :href="build.url" class="build-date">{{ build.date }}</a>
by
<a :href="build.author.webUrl" class="issue-author-link">
{{ build.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="build.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageTestComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component">
<div class="item-details">
<h5 class="item-title">
<span class="icon-build-status">${global.cycleAnalytics.svgs.iconBuildStatus}</span>
<a :href="build.url" class="item-build-name">{{ build.name }}</a>
&middot;
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i>
<a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
<span class="icon-branch">${global.cycleAnalytics.svgs.iconBranch}</span>
<a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
</h5>
<span>
<a :href="build.url" class="issue-date">
{{ build.date }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="build.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.TotalTimeComponent = Vue.extend({
props: {
time: Object,
},
template: `
<span class="total-time">
<template v-if="time.days">{{ time.days }} <span>{{ time.days === 1 ? 'day' : 'days' }}</span></template>
<template v-if="time.hours">{{ time.hours }} <span>hr</span></template>
<template v-if="time.mins && !time.days">{{ time.mins }} <span>mins</span></template>
<template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>s</span></template>
</span>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable */
//= require vue //= require vue
//= require_tree ./svg
((global) => { //= require_tree .
const COOKIE_NAME = 'cycle_analytics_help_dismissed'; $(() => {
const store = gl.cycleAnalyticsStore = { const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
isLoading: true, const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
hasError: false, const cycleAnalyticsStore = gl.cycleAnalytics.CycleAnalyticsStore;
isHelpDismissed: Cookies.get(COOKIE_NAME), const cycleAnalyticsService = new gl.cycleAnalytics.CycleAnalyticsService({
analytics: {} requestPath: cycleAnalyticsEl.dataset.requestPath,
}; });
gl.CycleAnalytics = class CycleAnalytics { gl.cycleAnalyticsApp = new Vue({
constructor() { el: '#cycle-analytics',
const that = this; name: 'CycleAnalytics',
data: {
this.vue = new Vue({ state: cycleAnalyticsStore.state,
el: '#cycle-analytics', isLoading: false,
name: 'CycleAnalytics', isLoadingStage: false,
created: this.fetchData(), isEmptyStage: false,
data: store, hasError: false,
methods: { startDate: 30,
dismissLanding() { isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE),
that.dismissLanding(); },
} computed: {
currentStage() {
return cycleAnalyticsStore.currentActiveStage();
},
},
components: {
'stage-issue-component': gl.cycleAnalytics.StageIssueComponent,
'stage-plan-component': gl.cycleAnalytics.StagePlanComponent,
'stage-code-component': gl.cycleAnalytics.StageCodeComponent,
'stage-test-component': gl.cycleAnalytics.StageTestComponent,
'stage-review-component': gl.cycleAnalytics.StageReviewComponent,
'stage-staging-component': gl.cycleAnalytics.StageStagingComponent,
'stage-production-component': gl.cycleAnalytics.StageProductionComponent,
},
created() {
this.fetchCycleAnalyticsData();
},
methods: {
handleError() {
cycleAnalyticsStore.setErrorState(true);
return new Flash('There was an error while fetching cycle analytics data.');
},
initDropdown() {
const $dropdown = $('.js-ca-dropdown');
const $label = $dropdown.find('.dropdown-label');
$dropdown.find('li a').off('click').on('click', (e) => {
e.preventDefault();
const $target = $(e.currentTarget);
this.startDate = $target.data('value');
$label.text($target.text().trim());
this.fetchCycleAnalyticsData({ startDate: this.startDate });
});
},
fetchCycleAnalyticsData(options) {
const fetchOptions = options || { startDate: this.startDate };
this.isLoading = true;
cycleAnalyticsService
.fetchCycleAnalyticsData(fetchOptions)
.done((response) => {
cycleAnalyticsStore.setCycleAnalyticsData(response);
this.selectDefaultStage();
this.initDropdown();
})
.error(() => {
this.handleError();
})
.always(() => {
this.isLoading = false;
});
},
selectDefaultStage() {
const stage = this.state.stages.first();
this.selectStage(stage);
},
selectStage(stage) {
if (this.isLoadingStage) return;
if (this.currentStage === stage) return;
if (!stage.isUserAllowed) {
cycleAnalyticsStore.setActiveStage(stage);
return;
} }
});
}
fetchData(options) {
store.isLoading = true;
options = options || { startDate: 30 };
$.ajax({
url: $('#cycle-analytics').data('request-path'),
method: 'GET',
dataType: 'json',
contentType: 'application/json',
data: {
cycle_analytics: {
start_date: options.startDate
}
}
}).done((data) => {
this.decorateData(data);
this.initDropdown();
})
.error((data) => {
this.handleError(data);
})
.always(() => {
store.isLoading = false;
})
}
decorateData(data) {
data.summary = data.summary || [];
data.stats = data.stats || [];
data.summary.forEach((item) => {
item.value = item.value || '-';
});
data.stats.forEach((item) => {
item.value = item.value || '- - -';
});
store.analytics = data;
}
handleError(data) {
store.hasError = true;
new Flash('There was an error while fetching cycle analytics data.', 'alert');
}
dismissLanding() {
store.isHelpDismissed = true;
Cookies.set(COOKIE_NAME, true);
}
initDropdown() {
const $dropdown = $('.js-ca-dropdown');
const $label = $dropdown.find('.dropdown-label');
$dropdown.find('li a').off('click').on('click', (e) => {
e.preventDefault();
const $target = $(e.currentTarget);
const value = $target.data('value');
$label.text($target.text().trim());
this.fetchData({ startDate: value });
})
}
}
})(window.gl || (window.gl = {})); this.isLoadingStage = true;
cycleAnalyticsStore.setStageEvents([]);
cycleAnalyticsStore.setActiveStage(stage);
cycleAnalyticsService
.fetchStageData({
stage,
startDate: this.startDate,
})
.done((response) => {
this.isEmptyStage = !response.events.length;
cycleAnalyticsStore.setStageEvents(response.events);
})
.error(() => {
this.isEmptyStage = true;
})
.always(() => {
this.isLoadingStage = false;
});
},
dismissOverviewDialog() {
this.isOverviewDialogDismissed = true;
Cookies.set(OVERVIEW_DIALOG_COOKIE, '1');
},
},
});
// Register global components
Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent);
});
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
class CycleAnalyticsService {
constructor(options) {
this.requestPath = options.requestPath;
}
fetchCycleAnalyticsData(options) {
options = options || { startDate: 30 };
return $.ajax({
url: this.requestPath,
method: 'GET',
dataType: 'json',
contentType: 'application/json',
data: {
cycle_analytics: {
start_date: options.startDate,
},
},
});
}
fetchStageData(options) {
const {
stage,
startDate,
} = options;
return $.get(`${this.requestPath}/events/${stage.title.toLowerCase()}.json`, {
cycle_analytics: {
start_date: startDate,
},
});
}
}
global.cycleAnalytics.CycleAnalyticsService = CycleAnalyticsService;
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
const EMPTY_STAGE_TEXTS = {
issue: 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.',
plan: 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.',
code: 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.',
test: 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.',
review: 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.',
staging: 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.',
production: 'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.',
};
global.cycleAnalytics.CycleAnalyticsStore = {
state: {
summary: '',
stats: '',
analytics: '',
events: [],
stages: [],
},
setCycleAnalyticsData(data) {
this.state = Object.assign(this.state, this.decorateData(data));
},
decorateData(data) {
const newData = {};
newData.stages = data.stats || [];
newData.summary = data.summary || [];
newData.summary.forEach((item) => {
item.value = item.value || '-';
});
newData.stages.forEach((item) => {
const stageName = item.title.toLowerCase();
item.active = false;
item.isUserAllowed = data.permissions[stageName];
item.emptyStageText = EMPTY_STAGE_TEXTS[stageName];
item.component = `stage-${stageName}-component`;
});
newData.analytics = data;
return newData;
},
setLoadingState(state) {
this.state.isLoading = state;
},
setErrorState(state) {
this.state.hasError = state;
},
deactivateAllStages() {
this.state.stages.forEach((stage) => {
stage.active = false;
});
},
setActiveStage(stage) {
this.deactivateAllStages();
stage.active = true;
},
setStageEvents(events) {
this.state.events = this.decorateEvents(events);
},
decorateEvents(events) {
const newEvents = events;
newEvents.forEach((item) => {
item.totalTime = item.total_time;
item.author.webUrl = item.author.web_url;
item.author.avatarUrl = item.author.avatar_url;
if (item.created_at) item.createdAt = item.created_at;
if (item.short_sha) item.shortSha = item.short_sha;
if (item.commit_url) item.commitUrl = item.commit_url;
delete item.author.web_url;
delete item.author.avatar_url;
delete item.total_time;
delete item.created_at;
delete item.short_sha;
delete item.commit_url;
});
return newEvents;
},
currentActiveStage() {
return this.state.stages.find(stage => stage.active);
},
};
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {};
global.cycleAnalytics.svgs.iconBranch = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><path fill="#8C8C8C" fill-rule="evenodd" d="M9.678 6.722C9.353 5.167 8.053 4 6.5 4S3.647 5.167 3.322 6.722h-2.6c-.397 0-.722.35-.722.778 0 .428.325.778.722.778h2.6C3.647 9.833 4.947 11 6.5 11s2.853-1.167 3.178-2.722h2.6c.397 0 .722-.35.722-.778 0-.428-.325-.778-.722-.778h-2.6zM4.694 7.5c0-1.09.795-1.944 1.806-1.944 1.01 0 1.806.855 1.806 1.944 0 1.09-.795 1.944-1.806 1.944-1.01 0-1.806-.855-1.806-1.944z"/></svg>';
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {};
global.cycleAnalytics.svgs.iconBuildStatus = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><g fill="#31AF64" fill-rule="evenodd"><path d="M12.5 7c0-3.038-2.462-5.5-5.5-5.5S1.5 3.962 1.5 7s2.462 5.5 5.5 5.5 5.5-2.462 5.5-5.5zM0 7c0-3.866 3.134-7 7-7s7 3.134 7 7-3.134 7-7 7-7-3.134-7-7z"/><path d="M6.28 7.697L5.045 6.464c-.117-.117-.305-.117-.42-.002l-.614.614c-.11.113-.11.303.007.42l1.91 1.91c.19.19.51.197.703.004l.264-.265L9.997 6.04c.108-.107.107-.293-.01-.408l-.612-.614c-.114-.113-.298-.12-.41-.01L6.28 7.7z"/></g></svg>';
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {};
global.cycleAnalytics.svgs.iconCommit = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><path fill="#8F8F8F" fill-rule="evenodd" d="M28.777 18c-.91-4.008-4.494-7-8.777-7-4.283 0-7.868 2.992-8.777 7H4.01C2.9 18 2 18.895 2 20c0 1.112.9 2 2.01 2h7.213c.91 4.008 4.494 7 8.777 7 4.283 0 7.868-2.992 8.777-7h7.214C37.1 22 38 21.105 38 20c0-1.112-.9-2-2.01-2h-7.213zM20 25c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5z"/></svg>';
})(window.gl || (window.gl = {}));
...@@ -110,10 +110,10 @@ ...@@ -110,10 +110,10 @@
Issuable.init(); Issuable.init();
break; break;
case 'dashboard:activity': case 'dashboard:activity':
new Activities(); new gl.Activities();
break; break;
case 'dashboard:projects:starred': case 'dashboard:projects:starred':
new Activities(); new gl.Activities();
break; break;
case 'projects:commit:show': case 'projects:commit:show':
new Commit(); new Commit();
...@@ -139,7 +139,7 @@ ...@@ -139,7 +139,7 @@
new gl.Pipelines(); new gl.Pipelines();
break; break;
case 'groups:activity': case 'groups:activity':
new Activities(); new gl.Activities();
break; break;
case 'groups:show': case 'groups:show':
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
...@@ -208,9 +208,6 @@ ...@@ -208,9 +208,6 @@
new gl.ProtectedBranchCreate(); new gl.ProtectedBranchCreate();
new gl.ProtectedBranchEditList(); new gl.ProtectedBranchEditList();
break; break;
case 'projects:cycle_analytics:show':
new gl.CycleAnalytics();
break;
} }
switch (path.first()) { switch (path.first()) {
case 'admin': case 'admin':
......
...@@ -157,17 +157,17 @@ ...@@ -157,17 +157,17 @@
<li v-bind:class="{ 'active': scope === undefined }"> <li v-bind:class="{ 'active': scope === undefined }">
<a :href="projectEnvironmentsPath"> <a :href="projectEnvironmentsPath">
Available Available
<span <span class="badge js-available-environments-count">
class="badge js-available-environments-count" {{state.availableCounter}}
v-html="state.availableCounter"></span> </span>
</a> </a>
</li> </li>
<li v-bind:class="{ 'active' : scope === 'stopped' }"> <li v-bind:class="{ 'active' : scope === 'stopped' }">
<a :href="projectStoppedEnvironmentsPath"> <a :href="projectStoppedEnvironmentsPath">
Stopped Stopped
<span <span class="badge js-stopped-environments-count">
class="badge js-stopped-environments-count" {{state.stoppedCounter}}
v-html="state.stoppedCounter"></span> </span>
</a> </a>
</li> </li>
</ul> </ul>
...@@ -183,8 +183,7 @@ ...@@ -183,8 +183,7 @@
<i class="fa fa-spinner spin"></i> <i class="fa fa-spinner spin"></i>
</div> </div>
<div <div class="blank-state blank-state-no-icon"
class="blank-state blank-state-no-icon"
v-if="!isLoading && state.environments.length === 0"> v-if="!isLoading && state.environments.length === 0">
<h2 class="blank-state-title"> <h2 class="blank-state-title">
You don't have any environments right now. You don't have any environments right now.
...@@ -205,8 +204,7 @@ ...@@ -205,8 +204,7 @@
</a> </a>
</div> </div>
<div <div class="table-holder"
class="table-holder"
v-if="!isLoading && state.environments.length > 0"> v-if="!isLoading && state.environments.length > 0">
<table class="table ci-table environments"> <table class="table ci-table environments">
<thead> <thead>
...@@ -234,7 +232,9 @@ ...@@ -234,7 +232,9 @@
is="environment-item" is="environment-item"
v-for="children in model.children" v-for="children in model.children"
:model="children" :model="children"
:toggleRow="toggleRow.bind(children)"> :toggleRow="toggleRow.bind(children)"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed">
</tr> </tr>
</template> </template>
......
...@@ -43,8 +43,7 @@ ...@@ -43,8 +43,7 @@
<div class="inline"> <div class="inline">
<div class="dropdown"> <div class="dropdown">
<a class="dropdown-new btn btn-default" data-toggle="dropdown"> <a class="dropdown-new btn btn-default" data-toggle="dropdown">
<span class="dropdown-play-icon-container"> <span class="dropdown-play-icon-container"></span>
</span>
<i class="fa fa-caret-down"></i> <i class="fa fa-caret-down"></i>
</a> </a>
...@@ -54,9 +53,10 @@ ...@@ -54,9 +53,10 @@
data-method="post" data-method="post"
rel="nofollow" rel="nofollow"
class="js-manual-action-link"> class="js-manual-action-link">
<span class="action-play-icon-container"> <span class="action-play-icon-container"></span>
<span>
{{action.name}}
</span> </span>
<span v-html="action.name"></span>
</a> </a>
</li> </li>
</ul> </ul>
......
...@@ -389,11 +389,10 @@ ...@@ -389,11 +389,10 @@
template: ` template: `
<tr> <tr>
<td v-bind:class="{ 'children-row': isChildren}"> <td v-bind:class="{ 'children-row': isChildren}">
<a <a v-if="!isFolder"
v-if="!isFolder"
class="environment-name" class="environment-name"
:href="model.environment_path" :href="model.environment_path">
v-html="model.name"> {{model.name}}
</a> </a>
<span v-else v-on:click="toggleRow(model)" class="folder-name"> <span v-else v-on:click="toggleRow(model)" class="folder-name">
<span class="folder-icon"> <span class="folder-icon">
...@@ -401,16 +400,19 @@ ...@@ -401,16 +400,19 @@
<i v-show="!model.isOpen" class="fa fa-caret-right"></i> <i v-show="!model.isOpen" class="fa fa-caret-right"></i>
</span> </span>
<span v-html="model.name"></span> <span>
{{model.name}}
</span>
<span class="badge" v-html="childrenCounter"></span> <span class="badge">
{{childrenCounter}}
</span>
</span> </span>
</td> </td>
<td class="deployment-column"> <td class="deployment-column">
<span <span v-if="shouldRenderDeploymentID">
v-if="shouldRenderDeploymentID" {{deploymentInternalId}}
v-html="deploymentInternalId">
</span> </span>
<span v-if="!isFolder && deploymentHasUser"> <span v-if="!isFolder && deploymentHasUser">
...@@ -427,8 +429,8 @@ ...@@ -427,8 +429,8 @@
<td> <td>
<a v-if="shouldRenderBuildName" <a v-if="shouldRenderBuildName"
class="build-link" class="build-link"
:href="model.last_deployment.deployable.build_path" :href="model.last_deployment.deployable.build_path">
v-html="buildName"> {{buildName}}
</a> </a>
</td> </td>
...@@ -451,8 +453,8 @@ ...@@ -451,8 +453,8 @@
<td> <td>
<span <span
v-if="!isFolder && model.last_deployment" v-if="!isFolder && model.last_deployment"
class="environment-created-date-timeago" class="environment-created-date-timeago">
v-html="createdDate"> {{createdDate}}
</span> </span>
</td> </td>
......
...@@ -14,8 +14,7 @@ ...@@ -14,8 +14,7 @@
}, },
template: ` template: `
<a <a class="btn stop-env-link"
class="btn stop-env-link"
:href="stop_url" :href="stop_url"
data-confirm="Are you sure you want to stop this environment?" data-confirm="Are you sure you want to stop this environment?"
data-method="post" data-method="post"
......
(() => {
/*
* TODO: Make these methods more configurable (e.g. parseSeconds timePeriodContstraints,
* stringifyTime condensed or non-condensed, abbreviateTimelengths)
* */
class PrettyTime {
/*
* Accepts seconds and returns a timeObject { weeks: #, days: #, hours: #, minutes: # }
* Seconds can be negative or positive, zero or non-zero.
*/
static parseSeconds(seconds) {
const DAYS_PER_WEEK = 5;
const HOURS_PER_DAY = 8;
const MINUTES_PER_HOUR = 60;
const MINUTES_PER_WEEK = DAYS_PER_WEEK * HOURS_PER_DAY * MINUTES_PER_HOUR;
const MINUTES_PER_DAY = HOURS_PER_DAY * MINUTES_PER_HOUR;
const timePeriodConstraints = {
weeks: MINUTES_PER_WEEK,
days: MINUTES_PER_DAY,
hours: MINUTES_PER_HOUR,
minutes: 1,
};
let unorderedMinutes = PrettyTime.secondsToMinutes(seconds);
return _.mapObject(timePeriodConstraints, (minutesPerPeriod) => {
const periodCount = Math.floor(unorderedMinutes / minutesPerPeriod);
unorderedMinutes -= (periodCount * minutesPerPeriod);
return periodCount;
});
}
/*
* Accepts a timeObject and returns a condensed string representation of it
* (e.g. '1w 2d 3h 1m' or '1h 30m'). Zero value units are not included.
*/
static stringifyTime(timeObject) {
const reducedTime = _.reduce(timeObject, (memo, unitValue, unitName) => {
const isNonZero = !!unitValue;
return isNonZero ? `${memo} ${unitValue}${unitName.charAt(0)}` : memo;
}, '').trim();
return reducedTime.length ? reducedTime : '0m';
}
/*
* Accepts a time string of any size (e.g. '1w 2d 3h 5m' or '1w 2d') and returns
* the first non-zero unit/value pair.
*/
static abbreviateTime(timeStr) {
return timeStr.split(' ')
.filter(unitStr => unitStr.charAt(0) !== '0')[0];
}
static secondsToMinutes(seconds) {
return Math.abs(seconds / 60);
}
}
gl.PrettyTime = PrettyTime;
})(window.gl || (window.gl = {}));
...@@ -218,7 +218,7 @@ ...@@ -218,7 +218,7 @@
} }
if (environment.deployed_at && environment.deployed_at_formatted) { if (environment.deployed_at && environment.deployed_at_formatted) {
environment.deployed_at = gl.utils.getTimeago(environment.deployed_at) + '.'; environment.deployed_at = gl.utils.getTimeago().format(environment.deployed_at, 'gl_en') + '.';
} else { } else {
$('.js-environment-timeago', $template).remove(); $('.js-environment-timeago', $template).remove();
environment.name += '.'; environment.name += '.';
......
/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-undef, prefer-template, wrap-iife, comma-dangle, no-return-assign, no-else-return, consistent-return, no-unused-vars, padded-blocks, max-len */
(function() {
this.Pager = {
init: function(limit, preload, disable, callback) {
this.limit = limit != null ? limit : 0;
this.disable = disable != null ? disable : false;
this.callback = callback != null ? callback : $.noop;
this.loading = $('.loading').first();
if (preload) {
this.offset = 0;
this.getOld();
} else {
this.offset = this.limit;
}
return this.initLoadMore();
},
getOld: function() {
this.loading.show();
return $.ajax({
type: "GET",
url: $(".content_list").data('href') || location.href,
data: "limit=" + this.limit + "&offset=" + this.offset,
complete: (function(_this) {
return function() {
return _this.loading.hide();
};
})(this),
success: function(data) {
Pager.append(data.count, data.html);
return Pager.callback();
},
dataType: "json"
});
},
append: function(count, html) {
$(".content_list").append(html);
if (count > 0) {
return this.offset += count;
} else {
return this.disable = true;
}
},
initLoadMore: function() {
$(document).unbind('scroll');
return $(document).endlessScroll({
bottomPixels: 400,
fireDelay: 1000,
fireOnce: true,
ceaseFire: function() {
return Pager.disable;
},
callback: (function(_this) {
return function(i) {
if (!_this.loading.is(':visible')) {
_this.loading.show();
return Pager.getOld();
}
};
})(this)
});
}
};
}).call(this);
(() => {
const ENDLESS_SCROLL_BOTTOM_PX = 400;
const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000;
const Pager = {
init(limit = 0, preload = false, disable = false, callback = $.noop) {
this.limit = limit;
this.offset = this.limit;
this.disable = disable;
this.callback = callback;
this.loading = $('.loading').first();
if (preload) {
this.offset = 0;
this.getOld();
}
this.initLoadMore();
},
getOld() {
this.loading.show();
$.ajax({
type: 'GET',
url: $('.content_list').data('href') || window.location.href,
data: `limit=${this.limit}&offset=${this.offset}`,
dataType: 'json',
error: () => this.loading.hide(),
success: (data) => {
this.append(data.count, data.html);
this.callback();
// keep loading until we've filled the viewport height
if (!this.disable && !this.isScrollable()) {
this.getOld();
} else {
this.loading.hide();
}
},
});
},
append(count, html) {
$('.content_list').append(html);
if (count > 0) {
this.offset += count;
} else {
this.disable = true;
}
},
isScrollable() {
const $w = $(window);
return $(document).height() > $w.height() + $w.scrollTop() + ENDLESS_SCROLL_BOTTOM_PX;
},
initLoadMore() {
$(document).unbind('scroll');
$(document).endlessScroll({
bottomPixels: ENDLESS_SCROLL_BOTTOM_PX,
fireDelay: ENDLESS_SCROLL_FIRE_DELAY_MS,
fireOnce: true,
ceaseFire: () => this.disable === true,
callback: () => {
if (!this.loading.is(':visible')) {
this.loading.show();
this.getOld();
}
},
});
},
};
window.Pager = Pager;
})();
/*
* Instances of SmartInterval extend the functionality of `setInterval`, make it configurable
* and controllable by a public API.
*
* */
(() => {
class SmartInterval {
/**
* @param { function } callback Function to be called on each iteration (required)
* @param { milliseconds } startingInterval `currentInterval` is set to this initially
* @param { milliseconds } maxInterval `currentInterval` will be incremented to this
* @param { integer } incrementByFactorOf `currentInterval` is incremented by this factor
* @param { boolean } lazyStart Configure if timer is initialized on instantiation or lazily
*/
constructor({ callback, startingInterval, maxInterval, incrementByFactorOf, lazyStart }) {
this.cfg = {
callback,
startingInterval,
maxInterval,
incrementByFactorOf,
lazyStart,
};
this.state = {
intervalId: null,
currentInterval: startingInterval,
pageVisibility: 'visible',
};
this.initInterval();
}
/* public */
start() {
const cfg = this.cfg;
const state = this.state;
state.intervalId = window.setInterval(() => {
cfg.callback();
if (this.getCurrentInterval() === cfg.maxInterval) {
return;
}
this.incrementInterval();
this.resume();
}, this.getCurrentInterval());
}
// cancel the existing timer, setting the currentInterval back to startingInterval
cancel() {
this.setCurrentInterval(this.cfg.startingInterval);
this.stopTimer();
}
// start a timer, using the existing interval
resume() {
this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped
this.start();
}
destroy() {
this.cancel();
$(document).off('visibilitychange').off('page:before-unload');
}
/* private */
initInterval() {
const cfg = this.cfg;
if (!cfg.lazyStart) {
this.start();
}
this.initVisibilityChangeHandling();
this.initPageUnloadHandling();
}
initVisibilityChangeHandling() {
// cancel interval when tab no longer shown (prevents cached pages from polling)
$(document)
.off('visibilitychange').on('visibilitychange', (e) => {
this.state.pageVisibility = e.target.visibilityState;
this.handleVisibilityChange();
});
}
initPageUnloadHandling() {
// prevent interval continuing after page change, when kept in cache by Turbolinks
$(document).on('page:before-unload', () => this.cancel());
}
handleVisibilityChange() {
const state = this.state;
const intervalAction = state.pageVisibility === 'hidden' ? this.cancel : this.resume;
intervalAction.apply(this);
}
getCurrentInterval() {
return this.state.currentInterval;
}
setCurrentInterval(newInterval) {
this.state.currentInterval = newInterval;
}
incrementInterval() {
const cfg = this.cfg;
const currentInterval = this.getCurrentInterval();
let nextInterval = currentInterval * cfg.incrementByFactorOf;
if (nextInterval > cfg.maxInterval) {
nextInterval = cfg.maxInterval;
}
this.setCurrentInterval(nextInterval);
}
stopTimer() {
const state = this.state;
state.intervalId = window.clearInterval(state.intervalId);
}
}
gl.SmartInterval = SmartInterval;
})(window.gl || (window.gl = {}));
//= require vue
//= require vue-resource
(() => {
/*
* SubbableResource can be extended to provide a pubsub-style service for one-off REST
* calls. Subscribe by passing a callback or render method you will use to handle responses.
*
* */
class SubbableResource {
constructor(resourcePath) {
this.endpoint = resourcePath;
// TODO: Switch to axios.create
this.resource = $.ajax;
this.subscribers = [];
}
subscribe(callback) {
this.subscribers.push(callback);
}
publish(newResponse) {
const responseCopy = _.extend({}, newResponse);
this.subscribers.forEach((fn) => {
fn(responseCopy);
});
return newResponse;
}
get(payload) {
return this.resource(payload)
.then(data => this.publish(data));
}
post(payload) {
return this.resource(payload)
.then(data => this.publish(data));
}
put(payload) {
return this.resource(payload)
.then(data => this.publish(data));
}
delete(payload) {
return this.resource(payload)
.then(data => this.publish(data));
}
}
gl.SubbableResource = SubbableResource;
})(window.gl || (window.gl = {}));
...@@ -134,7 +134,7 @@ content on the Users#show page. ...@@ -134,7 +134,7 @@ content on the Users#show page.
} }
const $calendarWrap = this.$parentEl.find('.user-calendar'); const $calendarWrap = this.$parentEl.find('.user-calendar');
$calendarWrap.load($calendarWrap.data('href')); $calendarWrap.load($calendarWrap.data('href'));
new Activities(); new gl.Activities();
return this.loaded['activity'] = true; return this.loaded['activity'] = true;
} }
......
...@@ -138,16 +138,15 @@ ...@@ -138,16 +138,15 @@
<a v-if="hasRef" <a v-if="hasRef"
class="monospace branch-name" class="monospace branch-name"
:href="ref.ref_url" :href="ref.ref_url">
v-html="ref.name"> {{ref.name}}
</a> </a>
<div class="icon-container commit-icon commit-icon-container"> <div class="icon-container commit-icon commit-icon-container"></div>
</div>
<a class="commit-id monospace" <a class="commit-id monospace"
:href="commit_url" :href="commit_url">
v-html="short_sha"> {{short_sha}}
</a> </a>
<p class="commit-title"> <p class="commit-title">
...@@ -156,14 +155,15 @@ ...@@ -156,14 +155,15 @@
class="avatar-image-container" class="avatar-image-container"
:href="author.web_url"> :href="author.web_url">
<img <img
class="avatar has-tooltip s20" class="avatar has-tooltip s20"
:src="author.avatar_url" :src="author.avatar_url"
:alt="userImageAltDescription" :alt="userImageAltDescription"
:title="author.username" /> :title="author.username" />
</a> </a>
<a class="commit-row-message" <a class="commit-row-message"
:href="commit_url" v-html="title"> :href="commit_url">
{{title}}
</a> </a>
</span> </span>
<span v-else> <span v-else>
......
...@@ -254,3 +254,32 @@ ...@@ -254,3 +254,32 @@
.content-block-small { .content-block-small {
padding: 10px 0; padding: 10px 0;
} }
.empty-state {
margin: 100px 0 0;
.text-content {
max-width: 460px;
margin: 0 auto;
padding: $gl-padding;
}
.svg-content {
text-align: center;
svg {
max-width: 425px;
width: 100%;
padding: $gl-padding;
}
}
@media(max-width: $screen-xs-max) {
margin-top: 50px;
text-align: center;
.btn {
width: 100%;
}
}
}
...@@ -160,6 +160,7 @@ $settings-icon-size: 18px; ...@@ -160,6 +160,7 @@ $settings-icon-size: 18px;
$provider-btn-group-border: #e5e5e5; $provider-btn-group-border: #e5e5e5;
$provider-btn-not-active-color: #4688f1; $provider-btn-not-active-color: #4688f1;
$link-underline-blue: #4a8bee; $link-underline-blue: #4a8bee;
$active-item-blue: #4a8bee;
$layout-link-gray: #7e7c7c; $layout-link-gray: #7e7c7c;
$todo-alert-blue: #428bca; $todo-alert-blue: #428bca;
$btn-side-margin: 10px; $btn-side-margin: 10px;
...@@ -283,6 +284,9 @@ $calendar-unselectable-bg: $gray-light; ...@@ -283,6 +284,9 @@ $calendar-unselectable-bg: $gray-light;
*/ */
$cycle-analytics-box-padding: 30px; $cycle-analytics-box-padding: 30px;
$cycle-analytics-box-text-color: #8c8c8c; $cycle-analytics-box-text-color: #8c8c8c;
$cycle-analytics-big-font: 19px;
$cycle-analytics-dark-text: $gl-title-color;
$cycle-analytics-light-gray: #bfbfbf;
/* /*
* Personal Access Tokens * Personal Access Tokens
......
#cycle-analytics { #cycle-analytics {
max-width: 1000px;
margin: 24px auto 0; margin: 24px auto 0;
max-width: 800px;
position: relative; position: relative;
.panel { .col-headers {
ul {
margin: 0;
padding: 0;
@include clearfix;
}
li {
display: inline-block;
float: left;
line-height: 50px;
width: 20%;
}
.fa {
color: $cycle-analytics-light-gray;
}
.stage-header {
width: 28%;
padding-left: $gl-padding;
}
.median-header {
width: 12%;
}
.event-header {
width: 45%;
padding-left: $gl-padding;
}
.total-time-header {
width: 15%;
text-align: right;
padding-right: $gl-padding;
}
.stage-name {
font-weight: 600;
}
}
.panel {
.content-block { .content-block {
padding: 24px 0; padding: 24px 0;
border-bottom: none; border-bottom: none;
...@@ -35,23 +78,20 @@ ...@@ -35,23 +78,20 @@
} }
&:last-child { &:last-child {
text-align: right;
@media (max-width: $screen-sm-min) { @media (max-width: $screen-sm-min) {
text-align: center; text-align: center;
} }
} }
} }
}
.dropdown { .js-ca-dropdown {
top: 13px; top: $gl-padding-top;
}
} }
.bordered-box { .bordered-box {
border: 1px solid $border-color; border: 1px solid $border-color;
border-radius: $border-radius-default; border-radius: $border-radius-default;
} }
.content-list { .content-list {
...@@ -141,4 +181,302 @@ ...@@ -141,4 +181,302 @@
margin-top: 36px; margin-top: 36px;
} }
.stage-panel-body {
display: flex;
flex-wrap: wrap;
}
.stage-nav,
.stage-entries {
display: flex;
vertical-align: top;
font-size: $gl-font-size;
}
.stage-nav {
width: 40%;
margin-bottom: 0;
ul {
padding: 0;
margin: 0;
width: 100%;
}
li {
list-style-type: none;
@include clearfix;
}
.stage-nav-item {
display: block;
line-height: 65px;
border-top: 1px solid transparent;
border-bottom: 1px solid transparent;
border-right: 1px solid $border-color;
background-color: $gray-light;
cursor: default;
&.active {
background-color: transparent;
border-right-color: transparent;
border-top-color: $border-color;
border-bottom-color: $border-color;
box-shadow: inset 2px 0 0 0 $active-item-blue;
.stage-name {
font-weight: 600;
}
}
&:hover:not(.active) {
background-color: $gray-lightest;
box-shadow: inset 2px 0 0 0 $border-color;
}
&:first-child {
border-top: none;
}
&:last-child {
border-bottom: none;
}
.stage-nav-item-cell {
float: left;
&.stage-name {
width: 70%;
}
&.stage-median {
width: 30%;
}
}
.stage-name {
padding-left: 16px;
}
.stage-empty,
.not-available {
color: $gl-text-color-light;
}
}
}
.stage-panel-container {
width: 100%;
overflow: auto;
}
.stage-panel {
min-width: 968px;
.panel-heading {
padding: 0;
background-color: transparent;
}
.events-description {
line-height: 65px;
padding-left: $gl-padding;
}
}
.stage-events {
width: 60%;
overflow: scroll;
height: 467px;
}
.stage-event-list {
margin: 0;
padding: 0;
}
.stage-event-item {
list-style-type: none;
padding: 0 0 $gl-padding;
margin: 0 $gl-padding $gl-padding;
border-bottom: 1px solid $gray-darker;
@include clearfix;
&:last-child {
border-bottom: none;
margin-bottom: 0;
}
.item-details,
.item-time {
float: left;
}
.item-details {
width: 75%;
}
.item-title {
margin: 0 0 2px;
&.issue-title,
&.commit-title,
&.merge-merquest-title {
max-width: 100%;
display: block;
@include text-overflow();
a {
color: $gl-dark-link-color;
}
}
}
.item-time {
width: 25%;
text-align: right;
}
.total-time {
font-size: $cycle-analytics-big-font;
color: $cycle-analytics-dark-text;
span {
color: $gl-text-color;
font-size: $gl-font-size;
}
}
.issue-date,
.build-date {
color: $gl-text-color;
}
.issue-link,
.commit-author-link,
.issue-author-link {
color: $gl-dark-link-color;
}
// Custom CSS for components
.item-conmmit-component {
.commit-icon {
position: relative;
top: 3px;
left: 1px;
display: inline-block;
svg {
float: left;
}
}
}
.merge-request-branch {
a {
max-width: 180px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: inline-block;
vertical-align: bottom;
}
}
}
// Custom Styles for stage items
.item-build-component {
.item-title {
.icon-build-status {
float: left;
margin-right: 5px;
position: relative;
top: 2px;
}
.item-build-name {
color: $gl-title-color;
}
.pipeline-id {
color: $gl-title-color;
padding: 0 3px 0 0;
}
.branch-name {
color: $black;
display: inline-block;
max-width: 180px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
line-height: 1.3;
vertical-align: top;
}
.short-sha {
color: $gl-link-color;
line-height: 1.3;
vertical-align: top;
font-weight: normal;
}
.fa {
color: $gl-text-color-light;
font-size: $code_font_size;
}
}
}
.empty-stage,
.no-access-stage {
text-align: center;
width: 75%;
margin: 0 auto;
padding-top: 130px;
color: $gl-text-color-light;
h4 {
color: $gl-text-color;
}
}
.empty-stage {
.icon-no-data {
height: 36px;
width: 78px;
display: inline-block;
margin-bottom: 20px;
}
}
.no-access-stage {
.icon-lock {
height: 36px;
width: 78px;
display: inline-block;
margin-bottom: 20px;
}
}
}
.cycle-analytics-overview {
padding-top: 100px;
.overview-details {
display: flex;
align-items: center;
}
.overview-image {
text-align: right;
}
.overview-icon {
svg {
width: 365px;
height: 227px;
}
}
} }
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
} }
.progress { .progress {
width: 100%;
height: 6px; height: 6px;
} }
} }
...@@ -30,7 +31,6 @@ ...@@ -30,7 +31,6 @@
margin-right: 7px; margin-right: 7px;
} }
// Issue title
span a { span a {
color: $gl-text-color; color: $gl-text-color;
word-wrap: break-word; word-wrap: break-word;
...@@ -39,15 +39,66 @@ ...@@ -39,15 +39,66 @@
} }
.milestone-summary { .milestone-summary {
margin-bottom: 25px;
.milestone-stat { .milestone-stat {
white-space: nowrap;
margin-right: 10px; margin-right: 10px;
&.with-drilldown {
margin-right: 2px;
}
} }
.remaining-days { .remaining-days {
color: $orange-light; color: $orange-light;
} }
.milestone-stats-and-buttons {
display: flex;
justify-content: flex-start;
flex-wrap: wrap;
@media (min-width: $screen-xs-min) {
justify-content: space-between;
flex-wrap: nowrap;
}
}
.milestone-progress-buttons {
order: 1;
margin-top: 10px;
@media (min-width: $screen-xs-min) {
order: 2;
margin-top: 0;
flex-shrink: 0;
}
.btn {
float: left;
margin-right: $btn-side-margin;
&:last-child {
margin-right: 0;
}
}
}
.milestone-stats {
order: 2;
width: 100%;
padding: 7px 0;
flex-shrink: 1;
@media (min-width: $screen-xs-min) {
// when displayed on one line stats go first, buttons second
order: 1;
}
}
.progress {
width: 100%;
margin: 15px 0;
}
} }
.issues-sortable-list, .issues-sortable-list,
...@@ -82,3 +133,50 @@ ...@@ -82,3 +133,50 @@
} }
} }
} }
.milestone-page-header {
display: flex;
flex-flow: row;
align-items: center;
flex-wrap: wrap;
.status-box {
margin-top: 0;
}
.milestone-buttons {
margin-left: auto;
}
.status-box {
order: 1;
}
.milestone-buttons {
order: 2;
}
.header-text-content {
order: 3;
width: 100%;
}
.milestone-buttons .verbose {
display: none;
}
@media (min-width: $screen-xs-min) {
.milestone-buttons .verbose {
display: inline;
}
.header-text-content {
order: 2;
width: auto;
}
.milestone-buttons {
order: 3;
}
}
}
...@@ -8,6 +8,10 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController ...@@ -8,6 +8,10 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
def show def show
@cycle_analytics = ::CycleAnalytics.new(@project, from: start_date(cycle_analytics_params)) @cycle_analytics = ::CycleAnalytics.new(@project, from: start_date(cycle_analytics_params))
stats_values, cycle_analytics_json = generate_cycle_analytics_data
@cycle_analytics_no_data = stats_values.blank?
respond_to do |format| respond_to do |format|
format.html format.html
format.json { render json: cycle_analytics_json } format.json { render json: cycle_analytics_json }
...@@ -22,23 +26,29 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController ...@@ -22,23 +26,29 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
{ start_date: params[:cycle_analytics][:start_date] } { start_date: params[:cycle_analytics][:start_date] }
end end
def cycle_analytics_json def generate_cycle_analytics_data
cycle_analytics_view_data = [[:issue, "Issue", "Time before an issue gets scheduled"], stats_values = []
[:plan, "Plan", "Time before an issue starts implementation"],
[:code, "Code", "Time until first merge request"], cycle_analytics_view_data = [[:issue, "Issue", "Related Issues", "Time before an issue gets scheduled"],
[:test, "Test", "Total test time for all commits/merges"], [:plan, "Plan", "Related Commits", "Time before an issue starts implementation"],
[:review, "Review", "Time between merge request creation and merge/close"], [:code, "Code", "Related Merge Requests", "Time spent coding"],
[:staging, "Staging", "From merge request merge until deploy to production"], [:test, "Test", "Relative Builds Trigger by Commits", "The time taken to build and test the application"],
[:production, "Production", "From issue creation until deploy to production"]] [:review, "Review", "Relative Merged Requests", "The time taken to review the code"],
[:staging, "Staging", "Relative Deployed Builds", "The time taken in staging"],
[:production, "Production", "Related Issues", "The total time taken from idea to production"]]
stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_description)| stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_legend, stage_description)|
value = @cycle_analytics.send(stage_method).presence value = @cycle_analytics.send(stage_method).presence
stats_values << value.abs if value
stats << { stats << {
title: stage_text, title: stage_text,
description: stage_description, description: stage_description,
legend: stage_legend,
value: value && !value.zero? ? distance_of_time_in_words(value) : nil value: value && !value.zero? ? distance_of_time_in_words(value) : nil
} }
stats stats
end end
...@@ -52,9 +62,11 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController ...@@ -52,9 +62,11 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
{ title: "Deploy".pluralize(deploys), value: deploys } { title: "Deploy".pluralize(deploys), value: deploys }
] ]
{ cycle_analytics_hash = { summary: summary,
summary: summary, stats: stats,
stats: stats permissions: @cycle_analytics.permissions(user: current_user)
} }
[stats_values, cycle_analytics_hash]
end end
end end
...@@ -82,12 +82,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -82,12 +82,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request_diff = @merge_request_diff =
if params[:diff_id] if params[:diff_id]
@merge_request.merge_request_diffs.find(params[:diff_id]) @merge_request.merge_request_diffs.viewable.find(params[:diff_id])
else else
@merge_request.merge_request_diff @merge_request.merge_request_diff
end end
@merge_request_diffs = @merge_request.merge_request_diffs.select_without_diff @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff
@comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id } @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
if params[:start_sha].present? if params[:start_sha].present?
...@@ -417,7 +417,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -417,7 +417,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
response = { response = {
title: merge_request.title, title: merge_request.title,
sha: merge_request.diff_head_commit.short_id, sha: (merge_request.diff_head_commit.short_id if merge_request.diff_head_sha),
status: status, status: status,
coverage: coverage coverage: coverage
} }
...@@ -564,7 +564,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -564,7 +564,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def define_pipelines_vars def define_pipelines_vars
@pipelines = @merge_request.all_pipelines @pipelines = @merge_request.all_pipelines
if @pipelines.present? if @pipelines.present? && @merge_request.commits.present?
@pipeline = @pipelines.first @pipeline = @pipelines.first
@statuses = @pipeline.statuses.relevant @statuses = @pipeline.statuses.relevant
end end
......
...@@ -50,14 +50,14 @@ module ApplicationSettingsHelper ...@@ -50,14 +50,14 @@ module ApplicationSettingsHelper
def restricted_level_checkboxes(help_block_id) def restricted_level_checkboxes(help_block_id)
Gitlab::VisibilityLevel.options.map do |name, level| Gitlab::VisibilityLevel.options.map do |name, level|
checked = restricted_visibility_levels(true).include?(level) checked = restricted_visibility_levels(true).include?(level)
css_class = 'btn' css_class = checked ? 'active' : ''
css_class += ' active' if checked checkbox_name = "application_setting[restricted_visibility_levels][]"
checkbox_name = 'application_setting[restricted_visibility_levels][]'
label_tag(checkbox_name, class: css_class) do label_tag(name, class: css_class) do
check_box_tag(checkbox_name, level, checked, check_box_tag(checkbox_name, level, checked,
autocomplete: 'off', autocomplete: 'off',
'aria-describedby' => help_block_id) + name 'aria-describedby' => help_block_id,
id: name) + visibility_level_icon(level) + name
end end
end end
end end
......
...@@ -48,4 +48,8 @@ module GroupsHelper ...@@ -48,4 +48,8 @@ module GroupsHelper
"#{status.humanize} #{projects_lfs_status(group)}" "#{status.humanize} #{projects_lfs_status(group)}"
end end
end end
def group_issues(group)
IssuesFinder.new(current_user, group_id: group.id).execute
end
end end
...@@ -455,4 +455,8 @@ module ProjectsHelper ...@@ -455,4 +455,8 @@ module ProjectsHelper
def project_child_container_class(view_path) def project_child_container_class(view_path)
view_path == "projects/issues/issues" ? "prepend-top-default" : "project-show-#{view_path}" view_path == "projects/issues/issues" ? "prepend-top-default" : "project-show-#{view_path}"
end end
def project_issues(project)
IssuesFinder.new(current_user, project_id: project.id).execute
end
end end
...@@ -487,6 +487,10 @@ module Ci ...@@ -487,6 +487,10 @@ module Ci
] ]
end end
def credentials
Gitlab::Ci::Build::Credentials::Factory.new(self).create!
end
private private
def update_artifacts_size def update_artifacts_size
......
# == Mentionable concern # == Mentionable concern
# #
# Contains functionality related to objects that can mention Users, Issues, MergeRequests, or Commits by # Contains functionality related to objects that can mention Users, Issues, MergeRequests, Commits or Snippets by
# GFM references. # GFM references.
# #
# Used by Issue, Note, MergeRequest, and Commit. # Used by Issue, Note, MergeRequest, and Commit.
......
class CycleAnalytics class CycleAnalytics
STAGES = %i[issue plan code test review staging production].freeze
def initialize(project, from:) def initialize(project, from:)
@project = project @project = project
@from = from @from = from
...@@ -9,6 +11,10 @@ class CycleAnalytics ...@@ -9,6 +11,10 @@ class CycleAnalytics
@summary ||= Summary.new(@project, from: @from) @summary ||= Summary.new(@project, from: @from)
end end
def permissions(user:)
Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project)
end
def issue def issue
@fetcher.calculate_metric(:issue, @fetcher.calculate_metric(:issue,
Issue.arel_table[:created_at], Issue.arel_table[:created_at],
......
...@@ -19,7 +19,7 @@ class Environment < ActiveRecord::Base ...@@ -19,7 +19,7 @@ class Environment < ActiveRecord::Base
allow_nil: true, allow_nil: true,
addressable_url: true addressable_url: true
delegate :stop_action, to: :last_deployment, allow_nil: true delegate :stop_action, :manual_actions, to: :last_deployment, allow_nil: true
scope :available, -> { with_state(:available) } scope :available, -> { with_state(:available) }
scope :stopped, -> { with_state(:stopped) } scope :stopped, -> { with_state(:stopped) }
...@@ -99,4 +99,12 @@ class Environment < ActiveRecord::Base ...@@ -99,4 +99,12 @@ class Environment < ActiveRecord::Base
stop stop
stop_action.play(current_user) stop_action.play(current_user)
end end
def actions_for(environment)
return [] unless manual_actions
manual_actions.select do |action|
action.expanded_environment_name == environment
end
end
end end
...@@ -11,6 +11,9 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -11,6 +11,9 @@ class MergeRequestDiff < ActiveRecord::Base
belongs_to :merge_request belongs_to :merge_request
serialize :st_commits
serialize :st_diffs
state_machine :state, initial: :empty do state_machine :state, initial: :empty do
state :collected state :collected
state :overflow state :overflow
...@@ -22,8 +25,7 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -22,8 +25,7 @@ class MergeRequestDiff < ActiveRecord::Base
state :overflow_diff_lines_limit state :overflow_diff_lines_limit
end end
serialize :st_commits scope :viewable, -> { without_state(:empty) }
serialize :st_diffs
# All diff information is collected from repository after object is created. # All diff information is collected from repository after object is created.
# It allows you to override variables like head_commit_sha before getting diff. # It allows you to override variables like head_commit_sha before getting diff.
......
...@@ -1086,7 +1086,7 @@ class Project < ActiveRecord::Base ...@@ -1086,7 +1086,7 @@ class Project < ActiveRecord::Base
"refs/heads/#{branch}", "refs/heads/#{branch}",
force: true) force: true)
repository.copy_gitattributes(branch) repository.copy_gitattributes(branch)
repository.expire_avatar_cache(branch) repository.expire_avatar_cache
reload_default_branch reload_default_branch
end end
......
...@@ -128,15 +128,9 @@ class JiraService < IssueTrackerService ...@@ -128,15 +128,9 @@ class JiraService < IssueTrackerService
return unless jira_issue.present? return unless jira_issue.present?
project = self.project noteable_id = noteable.respond_to?(:iid) ? noteable.iid : noteable.id
noteable_name = noteable.model_name.singular noteable_type = noteable_name(noteable)
noteable_id = if noteable.is_a?(Commit) entity_url = build_entity_url(noteable_type, noteable_id)
noteable.id
else
noteable.iid
end
entity_url = build_entity_url(noteable_name.to_sym, noteable_id)
data = { data = {
user: { user: {
...@@ -144,11 +138,11 @@ class JiraService < IssueTrackerService ...@@ -144,11 +138,11 @@ class JiraService < IssueTrackerService
url: resource_url(user_path(author)), url: resource_url(user_path(author)),
}, },
project: { project: {
name: project.path_with_namespace, name: self.project.path_with_namespace,
url: resource_url(namespace_project_path(project.namespace, project)) url: resource_url(namespace_project_path(project.namespace, self.project))
}, },
entity: { entity: {
name: noteable_name.humanize.downcase, name: noteable_type.humanize.downcase,
url: entity_url, url: entity_url,
title: noteable.title title: noteable.title
} }
...@@ -285,18 +279,26 @@ class JiraService < IssueTrackerService ...@@ -285,18 +279,26 @@ class JiraService < IssueTrackerService
"#{Settings.gitlab.base_url.chomp("/")}#{resource}" "#{Settings.gitlab.base_url.chomp("/")}#{resource}"
end end
def build_entity_url(entity_name, entity_id) def build_entity_url(noteable_type, entity_id)
polymorphic_url( polymorphic_url(
[ [
self.project.namespace.becomes(Namespace), self.project.namespace.becomes(Namespace),
self.project, self.project,
entity_name noteable_type.to_sym
], ],
id: entity_id, id: entity_id,
host: Settings.gitlab.base_url host: Settings.gitlab.base_url
) )
end end
def noteable_name(noteable)
name = noteable.model_name.singular
# ProjectSnippet inherits from Snippet class so it causes
# routing error building the URL.
name == "project_snippet" ? "snippet" : name
end
# Handle errors when doing JIRA API calls # Handle errors when doing JIRA API calls
def jira_request def jira_request
yield yield
......
This diff is collapsed.
...@@ -6,6 +6,7 @@ class Snippet < ActiveRecord::Base ...@@ -6,6 +6,7 @@ class Snippet < ActiveRecord::Base
include Referable include Referable
include Sortable include Sortable
include Awardable include Awardable
include Mentionable
cache_markdown_field :title, pipeline: :single_line cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :content cache_markdown_field :content
......
...@@ -18,7 +18,9 @@ class Tree ...@@ -18,7 +18,9 @@ class Tree
def readme def readme
return @readme if defined?(@readme) return @readme if defined?(@readme)
available_readmes = blobs.select(&:readme?) available_readmes = blobs.select do |blob|
Gitlab::FileDetector.type_of(blob.name) == :readme
end
previewable_readmes = available_readmes.select do |blob| previewable_readmes = available_readmes.select do |blob|
previewable?(blob.name) previewable?(blob.name)
......
require_relative 'base_service'
## ##
# Branch can be deleted either by DeleteBranchService # Branch can be deleted either by DeleteBranchService
# or by GitPushService. # or by GitPushService.
......
require_relative 'base_service'
class CreateBranchService < BaseService class CreateBranchService < BaseService
def execute(branch_name, ref, source_project: @project) def execute(branch_name, ref, source_project: @project)
valid_branch = Gitlab::GitRefValidator.validate(branch_name) valid_branch = Gitlab::GitRefValidator.validate(branch_name)
......
require_relative 'base_service'
class CreateDeploymentService < BaseService class CreateDeploymentService < BaseService
def execute(deployable = nil) def execute(deployable = nil)
return unless executable? return unless executable?
......
require_relative 'base_service'
class CreateReleaseService < BaseService class CreateReleaseService < BaseService
def execute(tag_name, release_description) def execute(tag_name, release_description)
repository = project.repository repository = project.repository
......
require_relative 'base_service'
class CreateTagService < BaseService class CreateTagService < BaseService
def execute(tag_name, target, message, release_description = nil) def execute(tag_name, target, message, release_description = nil)
valid_tag = Gitlab::GitRefValidator.validate(tag_name) valid_tag = Gitlab::GitRefValidator.validate(tag_name)
......
require_relative 'base_service'
class DeleteBranchService < BaseService class DeleteBranchService < BaseService
def execute(branch_name) def execute(branch_name)
repository = project.repository repository = project.repository
......
require_relative 'base_service'
class DeleteMergedBranchesService < BaseService class DeleteMergedBranchesService < BaseService
def async_execute def async_execute
DeleteMergedBranchesWorker.perform_async(project.id, current_user.id) DeleteMergedBranchesWorker.perform_async(project.id, current_user.id)
......
require_relative 'base_service'
class DeleteTagService < BaseService class DeleteTagService < BaseService
def execute(tag_name) def execute(tag_name)
repository = project.repository repository = project.repository
......
require_relative "base_service"
module Files module Files
class CreateDirService < Files::BaseService class CreateDirService < Files::BaseService
def commit def commit
......
require_relative "base_service"
module Files module Files
class CreateService < Files::BaseService class CreateService < Files::BaseService
def commit def commit
......
require_relative "base_service"
module Files module Files
class DeleteService < Files::BaseService class DeleteService < Files::BaseService
def commit def commit
......
require_relative "base_service"
module Files module Files
class MultiService < Files::BaseService class MultiService < Files::BaseService
class FileChangedError < StandardError; end class FileChangedError < StandardError; end
......
require_relative "base_service"
module Files module Files
class UpdateService < Files::BaseService class UpdateService < Files::BaseService
class FileChangedError < StandardError; end class FileChangedError < StandardError; end
......
...@@ -18,7 +18,7 @@ class GitPushService < BaseService ...@@ -18,7 +18,7 @@ class GitPushService < BaseService
# #
def execute def execute
@project.repository.after_create if @project.empty_repo? @project.repository.after_create if @project.empty_repo?
@project.repository.after_push_commit(branch_name, params[:newrev]) @project.repository.after_push_commit(branch_name)
if push_remove_branch? if push_remove_branch?
@project.repository.after_remove_branch @project.repository.after_remove_branch
...@@ -51,12 +51,32 @@ class GitPushService < BaseService ...@@ -51,12 +51,32 @@ class GitPushService < BaseService
execute_related_hooks execute_related_hooks
perform_housekeeping perform_housekeeping
update_caches
end end
def update_gitattributes def update_gitattributes
@project.repository.copy_gitattributes(params[:ref]) @project.repository.copy_gitattributes(params[:ref])
end end
def update_caches
if is_default_branch?
paths = Set.new
@push_commits.each do |commit|
commit.raw_diffs(deltas_only: true).each do |diff|
paths << diff.new_path
end
end
types = Gitlab::FileDetector.types_in_paths(paths.to_a)
else
types = []
end
ProjectCacheWorker.perform_async(@project.id, types)
end
protected protected
def execute_related_hooks def execute_related_hooks
...@@ -70,7 +90,6 @@ class GitPushService < BaseService ...@@ -70,7 +90,6 @@ class GitPushService < BaseService
@project.execute_hooks(build_push_data.dup, :push_hooks) @project.execute_hooks(build_push_data.dup, :push_hooks)
@project.execute_services(build_push_data.dup, :push_hooks) @project.execute_services(build_push_data.dup, :push_hooks)
Ci::CreatePipelineService.new(@project, current_user, build_push_data).execute Ci::CreatePipelineService.new(@project, current_user, build_push_data).execute
ProjectCacheWorker.perform_async(@project.id)
if push_remove_branch? if push_remove_branch?
AfterBranchDeleteService AfterBranchDeleteService
......
...@@ -60,7 +60,15 @@ module MergeRequests ...@@ -60,7 +60,15 @@ module MergeRequests
merge_requests = filter_merge_requests(merge_requests) merge_requests = filter_merge_requests(merge_requests)
merge_requests.each do |merge_request| merge_requests.each do |merge_request|
reload_diff(merge_request) unless branch_removed? if merge_request.source_branch == @branch_name || force_push?
merge_request.reload_diff
else
mr_commit_ids = merge_request.commits.map(&:id)
push_commit_ids = @commits.map(&:id)
matches = mr_commit_ids & push_commit_ids
merge_request.reload_diff if matches.any?
end
merge_request.mark_as_unchecked merge_request.mark_as_unchecked
end end
end end
...@@ -165,16 +173,5 @@ module MergeRequests ...@@ -165,16 +173,5 @@ module MergeRequests
def branch_removed? def branch_removed?
Gitlab::Git.blank_ref?(@newrev) Gitlab::Git.blank_ref?(@newrev)
end end
def reload_diff(merge_request)
if merge_request.source_branch == @branch_name || force_push?
merge_request.reload_diff
else
mr_commit_ids = merge_request.commits.map(&:id)
push_commit_ids = @commits.map(&:id)
matches = mr_commit_ids & push_commit_ids
merge_request.reload_diff if matches.any?
end
end
end end
end end
require_relative 'base_service'
require_relative 'reopen_service'
require_relative 'close_service'
module MergeRequests module MergeRequests
class UpdateService < MergeRequests::BaseService class UpdateService < MergeRequests::BaseService
def execute(merge_request) def execute(merge_request)
......
require_relative 'base_service'
class UpdateReleaseService < BaseService class UpdateReleaseService < BaseService
def execute(tag_name, release_description) def execute(tag_name, release_description)
repository = project.repository repository = project.repository
......
...@@ -22,9 +22,8 @@ ...@@ -22,9 +22,8 @@
.form-group .form-group
= f.label :restricted_visibility_levels, class: 'control-label col-sm-2' = f.label :restricted_visibility_levels, class: 'control-label col-sm-2'
.col-sm-10 .col-sm-10
- data_attrs = { toggle: 'buttons' } - restricted_level_checkboxes('restricted-visibility-help').each do |level|
.btn-group{ data: data_attrs } .checkbox
- restricted_level_checkboxes('restricted-visibility-help').each do |level|
= level = level
%span.help-block#restricted-visibility-help %span.help-block#restricted-visibility-help
Selected levels cannot be used by non-admin users for projects or snippets. Selected levels cannot be used by non-admin users for projects or snippets.
......
...@@ -3,24 +3,27 @@ ...@@ -3,24 +3,27 @@
- if current_user - if current_user
= auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@group.name} issues") = auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@group.name} issues")
.top-area - if group_issues(@group).exists?
= render 'shared/issuable/nav', type: :issues .top-area
.nav-controls = render 'shared/issuable/nav', type: :issues
- if current_user .nav-controls
= link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do - if current_user
= icon('rss') = link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do
%span.icon-label = icon('rss')
Subscribe %span.icon-label
= render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue" Subscribe
= render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
= render 'shared/issuable/filter', type: :issues = render 'shared/issuable/filter', type: :issues
.row-content-block.second-block .row-content-block.second-block
Only issues from Only issues from the
%strong #{@group.name} %strong #{@group.name}
group are listed here. group are listed here.
- if current_user - if current_user
To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page. To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page.
.prepend-top-default .prepend-top-default
= render 'shared/issues' = render 'shared/issues'
- else
= render 'shared/empty_states/issues', project_select_button: true
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
= chat_name.chat_name = chat_name.chat_name
%td %td
- if chat_name.last_used_at - if chat_name.last_used_at
time_ago_with_tooltip(chat_name.last_used_at) = time_ago_with_tooltip(chat_name.last_used_at)
- else - else
Never Never
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
= spinner = spinner
:javascript :javascript
var activity = new Activities(); var activity = new gl.Activities();
$(document).on('page:restore', function (event) { $(document).on('page:restore', function (event) {
activity.reloadActivities() activity.reloadActivities()
}) })
.empty-stage-container
.empty-stage
.icon-no-data
= custom_icon ('icon_no_data')
%h4 We don’t have enough data to show this stage.
%p
{{currentStage.emptyStageText}}
.no-access-stage-container
.no-access-stage
.icon-lock
= custom_icon ('icon_lock')
%h4 You need permission.
%p
Want to see the data? Please ask administrator for access.
.cycle-analytics-overview
.container
.row
.col-md-10.col-md-offset-1
.row.overview-details
.col-md-6.overview-text
%h4 Introducing Cycle Analytics
%p
Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.
To set up CA, you must first define a production environment by setting up your CI and then deploy to production.
%p
%a.btn{ href: help_page_path('user/project/cycle_analytics'), target: "_blank" } Read more
.col-md-6.overview-image
%span.overview-icon
= custom_icon ('icon_cycle_analytics_overview')
- @no_container = true - @no_container = true
- page_title "Cycle Analytics" - page_title "Cycle Analytics"
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_tag('cycle_analytics/cycle_analytics_bundle.js') = page_specific_javascript_tag("cycle_analytics/cycle_analytics_bundle.js")
= render "projects/pipelines/head" = render "projects/pipelines/head"
#cycle-analytics{class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) }} #cycle-analytics{ class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) } }
- if @cycle_analytics_no_data
.bordered-box.landing.content-block{"v-if" => "!isHelpDismissed"} .bordered-box.landing.content-block{"v-if" => "!isOverviewDialogDismissed"}
= icon('times', class: 'dismiss-icon', "@click" => "dismissLanding()") = icon("times", class: "dismiss-icon", "@click" => "dismissOverviewDialog()")
.row .row
.col-sm-3.col-xs-12.svg-container .col-sm-3.col-xs-12.svg-container
= custom_icon('icon_cycle_analytics_splash') = custom_icon('icon_cycle_analytics_splash')
.col-sm-8.col-xs-12.inner-content .col-sm-8.col-xs-12.inner-content
%h4 %h4
Introducing Cycle Analytics Introducing Cycle Analytics
%p %p
Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project. Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project.
= link_to "Read more", help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn'
= link_to "Read more", help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn'
= icon("spinner spin", "v-show" => "isLoading") = icon("spinner spin", "v-show" => "isLoading")
.wrapper{"v-show" => "!isLoading && !hasError"} .wrapper{"v-show" => "!isLoading && !hasError"}
.panel.panel-default .panel.panel-default
.panel-heading .panel-heading
Pipeline Health Pipeline Health
.content-block .content-block
.container-fluid .container-fluid
.row .row
.col-sm-3.col-xs-12.column{"v-for" => "item in analytics.summary"} .col-sm-3.col-xs-12.column{"v-for" => "item in state.summary"}
%h3.header {{item.value}} %h3.header {{item.value}}
%p.text {{item.title}} %p.text {{item.title}}
.col-sm-3.col-xs-12.column .col-sm-3.col-xs-12.column
.dropdown.inline.js-ca-dropdown .dropdown.inline.js-ca-dropdown
%button.dropdown-menu-toggle{"data-toggle" => "dropdown", :type => "button"} %button.dropdown-menu-toggle{"data-toggle" => "dropdown", :type => "button"}
...@@ -42,22 +37,54 @@ ...@@ -42,22 +37,54 @@
%i.fa.fa-chevron-down %i.fa.fa-chevron-down
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
%li %li
%a{'href' => "#", 'data-value' => '30'} %a{ "href" => "#", "data-value" => "30" }
Last 30 days Last 30 days
%li %li
%a{'href' => "#", 'data-value' => '90'} %a{ "href" => "#", "data-value" => "90" }
Last 90 days Last 90 days
.stage-panel-container
.bordered-box .panel.panel-default.stage-panel
%ul.content-list .panel-heading
%li{"v-for" => "item in analytics.stats"} %nav.col-headers
.container-fluid %ul
.row %li.stage-header
.col-xs-8.title-col %span.stage-name
%p.title Stage
{{item.title}} %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The phase of the development lifecycle.", "aria-hidden" => "true" }
%p.text %li.median-header
{{item.description}} %span.stage-name
.col-xs-4.value-col Median
%span %i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The value lying at the midpoint of a series of observed values. E.g., between 3, 5, 9, the median is 5. Between 3, 5, 7, 8, the median is (5+7)/2 = 6.", "aria-hidden" => "true" }
{{item.value}} %li.event-header
%span.stage-name
{{ currentStage ? currentStage.legend : 'Related Issues' }}
%i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The collection of events added to the data gathered for that stage.", "aria-hidden" => "true" }
%li.total-time-header
%span.stage-name
Total Time
%i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The time taken by each data entry gathered by that stage.", "aria-hidden" => "true" }
.stage-panel-body
%nav.stage-nav
%ul
%li.stage-nav-item{ ':class' => '{ active: stage.active }', '@click' => 'selectStage(stage)', "v-for" => "stage in state.stages" }
.stage-nav-item-cell.stage-name
{{ stage.title }}
.stage-nav-item-cell.stage-median
%template{ "v-if" => "stage.isUserAllowed" }
%span{ "v-if" => "stage.value" }
{{ stage.value }}
%span.stage-empty{ "v-else" => true }
Not enough data
%template{ "v-else" => true }
%span.not-available
Not available
.section.stage-events
%template{ "v-if" => "isLoadingStage" }
= icon("spinner spin")
%template{ "v-if" => "currentStage && !currentStage.isUserAllowed" }
= render partial: "no_access"
%template{ "v-else" => true }
%template{ "v-if" => "isEmptyStage && !isLoadingStage" }
= render partial: "empty_stage"
%template{ "v-if" => "state.events.length && !isLoadingStage && !isEmptyStage" }
%component{ ":is" => "currentStage.component", ":stage" => "currentStage", ":items" => "state.events" }
%ul.content-list.issues-list.issuable-list %ul.content-list.issues-list.issuable-list
= render partial: "projects/issues/issue", collection: @issues = render partial: "projects/issues/issue", collection: @issues
- if @issues.blank? - if @issues.blank?
%li = render 'shared/empty_states/issues'
.nothing-here-block No issues to show
- if @issues.present? - if @issues.present?
= paginate @issues, theme: "gitlab" = paginate @issues, theme: "gitlab"
...@@ -10,8 +10,8 @@ ...@@ -10,8 +10,8 @@
- if current_user - if current_user
= auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@project.name} issues") = auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@project.name} issues")
%div{ class: (container_class) } - if project_issues(@project).exists?
- if @project.issues.any? %div{ class: (container_class) }
.top-area .top-area
= render 'shared/issuable/nav', type: :issues = render 'shared/issuable/nav', type: :issues
.nav-controls .nav-controls
...@@ -36,21 +36,5 @@ ...@@ -36,21 +36,5 @@
= render 'issues' = render 'issues'
- if new_issue_email - if new_issue_email
= render 'issue_by_email', email: new_issue_email = render 'issue_by_email', email: new_issue_email
- else - else
.blank-state.blank-state-welcome = render 'shared/empty_states/issues', button_path: new_namespace_project_issue_path(@project.namespace, @project)
%h2.blank-state-title.blank-state-welcome-title
Welcome to GitLab Issues
%p.blank-state-text
Code, test, and deploy together
.blank-state
.blank-state-icon
= custom_icon("issues", size: 50)
%h3.blank-state-title
You don't have any issues right now.
%p.blank-state-text
Issues are the best way to track your project progress
- if can? current_user, :create_issue, @project
= link_to new_namespace_project_issue_path(@project.namespace, @project), class: "btn btn-new", title: "New Issue", id: "new_issue_link" do
New Issue
- if new_issue_email
= render 'issue_by_email', email: new_issue_email
...@@ -9,10 +9,10 @@ ...@@ -9,10 +9,10 @@
- if @project.archived? - if @project.archived?
= render 'projects/merge_requests/widget/open/archived' = render 'projects/merge_requests/widget/open/archived'
- elsif @merge_request.commits.blank?
= render 'projects/merge_requests/widget/open/nothing'
- elsif @merge_request.branch_missing? - elsif @merge_request.branch_missing?
= render 'projects/merge_requests/widget/open/missing_branch' = render 'projects/merge_requests/widget/open/missing_branch'
- elsif @merge_request.commits.blank?
= render 'projects/merge_requests/widget/open/nothing'
- elsif @merge_request.unchecked? - elsif @merge_request.unchecked?
= render 'projects/merge_requests/widget/open/check' = render 'projects/merge_requests/widget/open/check'
- elsif @merge_request.cannot_be_merged? && !resolved_conflicts - elsif @merge_request.cannot_be_merged? && !resolved_conflicts
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
= render "projects/issues/head" = render "projects/issues/head"
%div{ class: container_class } %div{ class: container_class }
.detail-page-header .detail-page-header.milestone-page-header
.status-box{ class: status_box_class(@milestone) } .status-box{ class: status_box_class(@milestone) }
- if @milestone.closed? - if @milestone.closed?
Closed Closed
...@@ -12,13 +12,14 @@ ...@@ -12,13 +12,14 @@
Past due Past due
- else - else
Open Open
%span.identifier .header-text-content
Milestone ##{@milestone.iid} %span.identifier
- if @milestone.expires_at Milestone ##{@milestone.iid}
%span.creator - if @milestone.expires_at
&middot; %span.creator
= @milestone.expires_at &middot;
.pull-right = @milestone.expires_at
.milestone-buttons
- if can?(current_user, :admin_milestone, @project) - if can?(current_user, :admin_milestone, @project)
- if @milestone.active? - if @milestone.active?
= link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped" = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, @milestone, milestone: {state_event: :close }), method: :put, class: "btn btn-close btn-nr btn-grouped"
......
...@@ -13,4 +13,4 @@ ...@@ -13,4 +13,4 @@
= render 'projects/issues/issue', issue: issue = render 'projects/issues/issue', issue: issue
= paginate @issues, theme: "gitlab" = paginate @issues, theme: "gitlab"
- else - else
.nothing-here-block No issues to show = render 'shared/empty_states/issues'
- button_path = local_assigns.fetch(:button_path, false)
- project_select_button = local_assigns.fetch(:project_select_button, false)
- has_button = button_path || project_select_button
.row.empty-state
.pull-right.col-xs-12{ class: "#{'col-sm-6' if has_button}" }
.svg-content
= render 'shared/empty_states/icons/issues.svg'
.col-xs-12{ class: "#{'col-sm-6' if has_button}" }
.text-content
- if has_button
%h4
The Issue Tracker is a good place to add things that need to be improved or solved in a project!
%p
An issue can be a bug, a todo or a feature request that needs to be discussed in a project.
Besides, issues are searchable and filterable.
- if project_select_button
= render 'shared/new_project_item_select', path: 'issues/new', label: 'New issue'
- else
= link_to 'New issue', button_path, class: 'btn btn-new', title: 'New issue', id: 'new_issue_link'
- else
%h4.text-center There are no issues to show.
This diff is collapsed.
<svg width="14px" height="10px" viewBox="322 21 14 10" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M330.078605,22.8166945 L335.259532,29.6235062 C335.615145,30.0907182 335.412062,30.4694683 334.822641,30.4694683 L331.657805,30.4694683 L324.04678,30.4694683 C323.449879,30.4694683 323.260751,30.0822112 323.609889,29.6235062 L328.790816,22.8166945 C329.146429,22.3494825 329.729467,22.3579895 330.078605,22.8166945 Z" id="delta" stroke="#5C5C5C" stroke-width="1" fill="none"></path>
</svg>
This diff is collapsed.
<svg width="46px" height="54px" viewBox="227 0 46 54" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 41 (35326) - http://www.bohemiancoding.com/sketch -->
<desc>Created with Sketch.</desc>
<defs>
<rect id="path-1" x="0" y="20" width="46" height="34" rx="8"></rect>
<mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="46" height="34" fill="white">
<use xlink:href="#path-1"></use>
</mask>
<path d="M29,16 C29,8.2680135 22.7319865,2 15,2 C7.2680135,2 1,8.2680135 1,16" id="path-3"></path>
<mask id="mask-4" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="28" height="14" fill="white">
<use xlink:href="#path-3"></use>
</mask>
</defs>
<g id="locker" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(227.000000, 0.000000)">
<g id="Group-8">
<use id="Rectangle-14" stroke="#B5A7DD" mask="url(#mask-2)" stroke-width="6" xlink:href="#path-1"></use>
<g id="Group-7" transform="translate(8.000000, 0.000000)">
<use id="Oval-3" stroke="#B5A7DD" mask="url(#mask-4)" stroke-width="6" xlink:href="#path-3"></use>
<rect id="Rectangle-13" fill="#B5A7DD" x="1" y="16" width="3" height="6"></rect>
<rect id="Rectangle-13-Copy" fill="#B5A7DD" x="26" y="16" width="3" height="6"></rect>
</g>
<path d="M25,37.4648712 C26.1956027,36.7732524 27,35.4805647 27,34 C27,31.790861 25.209139,30 23,30 C20.790861,30 19,31.790861 19,34 C19,35.4805647 19.8043973,36.7732524 21,37.4648712 L21,41.0026083 C21,42.1041422 21.8954305,43 23,43 C24.1122704,43 25,42.1057373 25,41.0026083 L25,37.4648712 Z" id="Combined-Shape" fill="#6B4FBB"></path>
</g>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="211 0 78 36" xmlns:xlink="http://www.w3.org/1999/xlink">
<defs>
<circle id="a" cx="5" cy="31" r="5"/>
<mask id="e" width="10" height="10" x="0" y="0" fill="#fff">
<use xlink:href="#a"/>
</mask>
<circle id="b" cx="29" cy="14" r="5"/>
<mask id="f" width="10" height="10" x="0" y="0" fill="#fff">
<use xlink:href="#b"/>
</mask>
<circle id="c" cx="53" cy="24" r="5"/>
<mask id="g" width="10" height="10" x="0" y="0" fill="#fff">
<use xlink:href="#c"/>
</mask>
<circle id="d" cx="73" cy="5" r="5"/>
<mask id="h" width="10" height="10" x="0" y="0" fill="#fff">
<use xlink:href="#d"/>
</mask>
</defs>
<g fill="none" fill-rule="evenodd" transform="translate(211)">
<path stroke="#B5A7DD" stroke-width="2" d="M5 31l24-17 26 10L73 5" stroke-linecap="round" stroke-dasharray="3 6"/>
<use fill="#FFF" stroke="#6B4FBB" stroke-width="6" mask="url(#e)" xlink:href="#a"/>
<use fill="#FFF" stroke="#6B4FBB" stroke-width="6" mask="url(#f)" xlink:href="#b"/>
<use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#g)" xlink:href="#c"/>
<use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#h)" xlink:href="#d"/>
</g>
</svg>
...@@ -3,32 +3,38 @@ ...@@ -3,32 +3,38 @@
.context.prepend-top-default .context.prepend-top-default
.milestone-summary .milestone-summary
%h4 Progress %h4 Progress
%strong= milestone.issues_visible_to_user(current_user).size
issues:
%span.milestone-stat
%strong= milestone.issues_visible_to_user(current_user).opened.size
open and
%strong= milestone.issues_visible_to_user(current_user).closed.size
closed
%strong= milestone.merge_requests.size
merge requests:
%span.milestone-stat
%strong= milestone.merge_requests.opened.size
open and
%strong= milestone.merge_requests.merged.size
merged
%span.milestone-stat
%strong== #{milestone.percent_complete(current_user)}%
complete
%span.milestone-stat .milestone-stats-and-buttons
%span.remaining-days= milestone_remaining_days(milestone) .milestone-stats
%span.pull-right.tab-issues-buttons %span.milestone-stat.with-drilldown
- if project && can?(current_user, :create_issue, project) %strong= milestone.issues_visible_to_user(current_user).size
= link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn btn-grouped", title: "New Issue" do issues:
New Issue %span.milestone-stat
= link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn btn-grouped" %strong= milestone.issues_visible_to_user(current_user).opened.size
%span.pull-right.tab-merge-requests-buttons.hidden open and
= link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn btn-grouped" %strong= milestone.issues_visible_to_user(current_user).closed.size
closed
%span.milestone-stat.with-drilldown
%strong= milestone.merge_requests.size
merge requests:
%span.milestone-stat
%strong= milestone.merge_requests.opened.size
open and
%strong= milestone.merge_requests.merged.size
merged
%span.milestone-stat
%strong== #{milestone.percent_complete(current_user)}%
complete
%span.milestone-stat
%span.remaining-days= milestone_remaining_days(milestone)
= milestone_progress_bar(milestone) .milestone-progress-buttons
%span.tab-issues-buttons
- if project && can?(current_user, :create_issue, project)
= link_to new_namespace_project_issue_path(project.namespace, project, issue: { milestone_id: milestone.id }), class: "btn", title: "New Issue" do
New Issue
= link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn"
%span.tab-merge-requests-buttons.hidden
= link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn"
= milestone_progress_bar(milestone)
...@@ -2,7 +2,9 @@ class NewNoteWorker ...@@ -2,7 +2,9 @@ class NewNoteWorker
include Sidekiq::Worker include Sidekiq::Worker
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
def perform(note_id) # Keep extra parameter to preserve backwards compatibility with
# old `NewNoteWorker` jobs (can remove later)
def perform(note_id, _params = {})
if note = Note.find_by(id: note_id) if note = Note.find_by(id: note_id)
NotificationService.new.new_note(note) NotificationService.new.new_note(note)
Notes::PostProcessService.new(note).execute Notes::PostProcessService.new(note).execute
......
# Worker for updating any project specific caches. # Worker for updating any project specific caches.
#
# This worker runs at most once every 15 minutes per project. This is to ensure
# that multiple instances of jobs for this worker don't hammer the underlying
# storage engine as much.
class ProjectCacheWorker class ProjectCacheWorker
include Sidekiq::Worker include Sidekiq::Worker
include DedicatedSidekiqQueue include DedicatedSidekiqQueue
LEASE_TIMEOUT = 15.minutes.to_i LEASE_TIMEOUT = 15.minutes.to_i
def self.lease_for(project_id) # project_id - The ID of the project for which to flush the cache.
Gitlab::ExclusiveLease. # refresh - An Array containing extra types of data to refresh such as
new("project_cache_worker:#{project_id}", timeout: LEASE_TIMEOUT) # `:readme` to flush the README and `:changelog` to flush the
end # CHANGELOG.
def perform(project_id, refresh = [])
project = Project.find_by(id: project_id)
# Overwrite Sidekiq's implementation so we only schedule when actually needed. return unless project && project.repository.exists?
def self.perform_async(project_id)
# If a lease for this project is still being held there's no point in
# scheduling a new job.
super unless lease_for(project_id).exists?
end
def perform(project_id) update_repository_size(project)
if try_obtain_lease_for(project_id) project.update_commit_count
Rails.logger.
info("Obtained ProjectCacheWorker lease for project #{project_id}")
else
Rails.logger.
info("Could not obtain ProjectCacheWorker lease for project #{project_id}")
return
end
update_caches(project_id) project.repository.refresh_method_caches(refresh.map(&:to_sym))
end end
def update_caches(project_id) def update_repository_size(project)
project = Project.find(project_id) return unless try_obtain_lease_for(project.id, :update_repository_size)
return unless project.repository.exists? Rails.logger.info("Updating repository size for project #{project.id}")
project.update_repository_size project.update_repository_size
project.update_commit_count
if project.repository.root_ref
project.repository.build_cache
end
end end
def try_obtain_lease_for(project_id) private
self.class.lease_for(project_id).try_obtain
def try_obtain_lease_for(project_id, section)
Gitlab::ExclusiveLease.
new("project_cache_worker:#{project_id}:#{section}", timeout: LEASE_TIMEOUT).
try_obtain
end end
end end
---
title: Changed restricted visibility admin buttons to checkboxes
merge_request: 7463
author:
---
title: Show events per stage on Cycle Analytics page
merge_request: 23449
author:
---
title: Fix activity page endless scroll on large viewports
merge_request: 7608
author:
---
title: Add deployment command to ChatOps
merge_request: 7619
author:
---
title: Fix 500 error when group name ends with git
merge_request: 7630
author:
---
title: Send credentials (currently for registry only) with build data to GitLab Runner
merge_request: 7474
author:
---
title: Fix undefined error in CI linter
merge_request: 7650
author:
---
title: Added permissions per stage to cycle analytics endpoint
merge_request:
author:
---
title: Do not create a MergeRequestDiff record when source branch is deleted
merge_request: 7481
author:
---
title: Fix errors happening when source branch of merge request is removed and then restored
merge_request: 7568
author:
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