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 ...
This commit is contained in:
commit
17388eb034
|
@ -23,7 +23,9 @@
|
|||
"spyOn": false,
|
||||
"spyOnEvent": false,
|
||||
"Turbolinks": false,
|
||||
"window": false
|
||||
"window": false,
|
||||
"Vue": false,
|
||||
"Flash": false,
|
||||
"Cookies": false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -271,12 +271,17 @@ rake db:seed_fu:
|
|||
- log/development.log
|
||||
|
||||
teaspoon:
|
||||
cache:
|
||||
paths:
|
||||
- vendor/ruby
|
||||
- node_modules/
|
||||
stage: test
|
||||
<<: *use-db
|
||||
script:
|
||||
- curl --silent --location https://deb.nodesource.com/setup_6.x | bash -
|
||||
- apt-get install --assume-yes nodejs
|
||||
- npm install --global istanbul
|
||||
- npm install
|
||||
- npm link istanbul
|
||||
- rake teaspoon
|
||||
artifacts:
|
||||
name: coverage-javascript
|
||||
|
@ -319,12 +324,11 @@ migration paths:
|
|||
- master@gitlab/gitlabhq
|
||||
- master@gitlab/gitlab-ee
|
||||
script:
|
||||
- git checkout HEAD .
|
||||
- git fetch --tags
|
||||
- git checkout v8.5.9
|
||||
- git fetch origin v8.5.9
|
||||
- git checkout -f FETCH_HEAD
|
||||
- cp config/resque.yml.example 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
|
||||
- git checkout $CI_BUILD_REF
|
||||
- source scripts/prepare_build.sh
|
||||
|
@ -346,8 +350,11 @@ coverage:
|
|||
- coverage/assets/
|
||||
|
||||
lint-javascript:
|
||||
cache:
|
||||
paths:
|
||||
- node_modules/
|
||||
stage: test
|
||||
image: "node:latest"
|
||||
image: "node:7.1"
|
||||
before_script:
|
||||
- npm install
|
||||
script:
|
||||
|
|
|
@ -32,6 +32,7 @@ entry.
|
|||
- Fix sidekiq stats in admin area (blackst0ne)
|
||||
- Added label description as tooltip to issue board list title
|
||||
- Created cycle analytics bundle JavaScript file
|
||||
- Make the milestone page more responsive (yury-n)
|
||||
- Hides container registry when repository is disabled
|
||||
- API: Fix booleans not recognized as such when using the `to_boolean` helper
|
||||
- Removed delete branch tooltip !6954
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
/* 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);
|
|
@ -0,0 +1,36 @@
|
|||
/* 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 = {}));
|
|
@ -0,0 +1,43 @@
|
|||
/* 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>
|
||||
·
|
||||
<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 = {}));
|
|
@ -0,0 +1,45 @@
|
|||
/* 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>
|
||||
·
|
||||
<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 = {}));
|
|
@ -0,0 +1,42 @@
|
|||
/* 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 = {}));
|
|
@ -0,0 +1,45 @@
|
|||
/* 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>
|
||||
·
|
||||
<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 = {}));
|
|
@ -0,0 +1,55 @@
|
|||
/* 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>
|
||||
·
|
||||
<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 = {}));
|
|
@ -0,0 +1,42 @@
|
|||
/* 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 = {}));
|
|
@ -0,0 +1,42 @@
|
|||
/* 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>
|
||||
·
|
||||
<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 = {}));
|
|
@ -0,0 +1,18 @@
|
|||
/* 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 = {}));
|
|
@ -1,98 +1,121 @@
|
|||
/* eslint-disable */
|
||||
//= require vue
|
||||
//= require_tree ./svg
|
||||
//= require_tree .
|
||||
|
||||
((global) => {
|
||||
$(() => {
|
||||
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
|
||||
const cycleAnalyticsEl = document.querySelector('#cycle-analytics');
|
||||
const cycleAnalyticsStore = gl.cycleAnalytics.CycleAnalyticsStore;
|
||||
const cycleAnalyticsService = new gl.cycleAnalytics.CycleAnalyticsService({
|
||||
requestPath: cycleAnalyticsEl.dataset.requestPath,
|
||||
});
|
||||
|
||||
const COOKIE_NAME = 'cycle_analytics_help_dismissed';
|
||||
const store = gl.cycleAnalyticsStore = {
|
||||
isLoading: true,
|
||||
hasError: false,
|
||||
isHelpDismissed: Cookies.get(COOKIE_NAME),
|
||||
analytics: {}
|
||||
};
|
||||
gl.cycleAnalyticsApp = new Vue({
|
||||
el: '#cycle-analytics',
|
||||
name: 'CycleAnalytics',
|
||||
data: {
|
||||
state: cycleAnalyticsStore.state,
|
||||
isLoading: false,
|
||||
isLoadingStage: false,
|
||||
isEmptyStage: false,
|
||||
hasError: false,
|
||||
startDate: 30,
|
||||
isOverviewDialogDismissed: Cookies.get(OVERVIEW_DIALOG_COOKIE),
|
||||
},
|
||||
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');
|
||||
|
||||
gl.CycleAnalytics = class CycleAnalytics {
|
||||
constructor() {
|
||||
const that = this;
|
||||
$dropdown.find('li a').off('click').on('click', (e) => {
|
||||
e.preventDefault();
|
||||
const $target = $(e.currentTarget);
|
||||
this.startDate = $target.data('value');
|
||||
|
||||
this.vue = new Vue({
|
||||
el: '#cycle-analytics',
|
||||
name: 'CycleAnalytics',
|
||||
created: this.fetchData(),
|
||||
data: store,
|
||||
methods: {
|
||||
dismissLanding() {
|
||||
that.dismissLanding();
|
||||
}
|
||||
$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 };
|
||||
this.isLoadingStage = true;
|
||||
cycleAnalyticsStore.setStageEvents([]);
|
||||
cycleAnalyticsStore.setActiveStage(stage);
|
||||
|
||||
$.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;
|
||||
})
|
||||
}
|
||||
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');
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
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 = {}));
|
||||
// Register global components
|
||||
Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
/* 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 = {}));
|
|
@ -0,0 +1,90 @@
|
|||
/* 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 = {}));
|
|
@ -0,0 +1,7 @@
|
|||
/* 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 = {}));
|
|
@ -0,0 +1,7 @@
|
|||
/* 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 = {}));
|
|
@ -0,0 +1,7 @@
|
|||
/* 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 @@
|
|||
Issuable.init();
|
||||
break;
|
||||
case 'dashboard:activity':
|
||||
new Activities();
|
||||
new gl.Activities();
|
||||
break;
|
||||
case 'dashboard:projects:starred':
|
||||
new Activities();
|
||||
new gl.Activities();
|
||||
break;
|
||||
case 'projects:commit:show':
|
||||
new Commit();
|
||||
|
@ -139,7 +139,7 @@
|
|||
new gl.Pipelines();
|
||||
break;
|
||||
case 'groups:activity':
|
||||
new Activities();
|
||||
new gl.Activities();
|
||||
break;
|
||||
case 'groups:show':
|
||||
shortcut_handler = new ShortcutsNavigation();
|
||||
|
@ -208,9 +208,6 @@
|
|||
new gl.ProtectedBranchCreate();
|
||||
new gl.ProtectedBranchEditList();
|
||||
break;
|
||||
case 'projects:cycle_analytics:show':
|
||||
new gl.CycleAnalytics();
|
||||
break;
|
||||
}
|
||||
switch (path.first()) {
|
||||
case 'admin':
|
||||
|
|
|
@ -157,17 +157,17 @@
|
|||
<li v-bind:class="{ 'active': scope === undefined }">
|
||||
<a :href="projectEnvironmentsPath">
|
||||
Available
|
||||
<span
|
||||
class="badge js-available-environments-count"
|
||||
v-html="state.availableCounter"></span>
|
||||
<span class="badge js-available-environments-count">
|
||||
{{state.availableCounter}}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li v-bind:class="{ 'active' : scope === 'stopped' }">
|
||||
<a :href="projectStoppedEnvironmentsPath">
|
||||
Stopped
|
||||
<span
|
||||
class="badge js-stopped-environments-count"
|
||||
v-html="state.stoppedCounter"></span>
|
||||
<span class="badge js-stopped-environments-count">
|
||||
{{state.stoppedCounter}}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -183,8 +183,7 @@
|
|||
<i class="fa fa-spinner spin"></i>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="blank-state blank-state-no-icon"
|
||||
<div class="blank-state blank-state-no-icon"
|
||||
v-if="!isLoading && state.environments.length === 0">
|
||||
<h2 class="blank-state-title">
|
||||
You don't have any environments right now.
|
||||
|
@ -205,8 +204,7 @@
|
|||
</a>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="table-holder"
|
||||
<div class="table-holder"
|
||||
v-if="!isLoading && state.environments.length > 0">
|
||||
<table class="table ci-table environments">
|
||||
<thead>
|
||||
|
@ -234,7 +232,9 @@
|
|||
is="environment-item"
|
||||
v-for="children in model.children"
|
||||
:model="children"
|
||||
:toggleRow="toggleRow.bind(children)">
|
||||
:toggleRow="toggleRow.bind(children)"
|
||||
:can-create-deployment="canCreateDeploymentParsed"
|
||||
:can-read-environment="canReadEnvironmentParsed">
|
||||
</tr>
|
||||
|
||||
</template>
|
||||
|
|
|
@ -43,8 +43,7 @@
|
|||
<div class="inline">
|
||||
<div class="dropdown">
|
||||
<a class="dropdown-new btn btn-default" data-toggle="dropdown">
|
||||
<span class="dropdown-play-icon-container">
|
||||
</span>
|
||||
<span class="dropdown-play-icon-container"></span>
|
||||
<i class="fa fa-caret-down"></i>
|
||||
</a>
|
||||
|
||||
|
@ -54,9 +53,10 @@
|
|||
data-method="post"
|
||||
rel="nofollow"
|
||||
class="js-manual-action-link">
|
||||
<span class="action-play-icon-container">
|
||||
<span class="action-play-icon-container"></span>
|
||||
<span>
|
||||
{{action.name}}
|
||||
</span>
|
||||
<span v-html="action.name"></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
|
|
@ -389,11 +389,10 @@
|
|||
template: `
|
||||
<tr>
|
||||
<td v-bind:class="{ 'children-row': isChildren}">
|
||||
<a
|
||||
v-if="!isFolder"
|
||||
<a v-if="!isFolder"
|
||||
class="environment-name"
|
||||
:href="model.environment_path"
|
||||
v-html="model.name">
|
||||
:href="model.environment_path">
|
||||
{{model.name}}
|
||||
</a>
|
||||
<span v-else v-on:click="toggleRow(model)" class="folder-name">
|
||||
<span class="folder-icon">
|
||||
|
@ -401,16 +400,19 @@
|
|||
<i v-show="!model.isOpen" class="fa fa-caret-right"></i>
|
||||
</span>
|
||||
|
||||
<span v-html="model.name"></span>
|
||||
<span>
|
||||
{{model.name}}
|
||||
</span>
|
||||
|
||||
<span class="badge" v-html="childrenCounter"></span>
|
||||
<span class="badge">
|
||||
{{childrenCounter}}
|
||||
</span>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="deployment-column">
|
||||
<span
|
||||
v-if="shouldRenderDeploymentID"
|
||||
v-html="deploymentInternalId">
|
||||
<span v-if="shouldRenderDeploymentID">
|
||||
{{deploymentInternalId}}
|
||||
</span>
|
||||
|
||||
<span v-if="!isFolder && deploymentHasUser">
|
||||
|
@ -427,8 +429,8 @@
|
|||
<td>
|
||||
<a v-if="shouldRenderBuildName"
|
||||
class="build-link"
|
||||
:href="model.last_deployment.deployable.build_path"
|
||||
v-html="buildName">
|
||||
:href="model.last_deployment.deployable.build_path">
|
||||
{{buildName}}
|
||||
</a>
|
||||
</td>
|
||||
|
||||
|
@ -451,8 +453,8 @@
|
|||
<td>
|
||||
<span
|
||||
v-if="!isFolder && model.last_deployment"
|
||||
class="environment-created-date-timeago"
|
||||
v-html="createdDate">
|
||||
class="environment-created-date-timeago">
|
||||
{{createdDate}}
|
||||
</span>
|
||||
</td>
|
||||
|
||||
|
|
|
@ -14,8 +14,7 @@
|
|||
},
|
||||
|
||||
template: `
|
||||
<a
|
||||
class="btn stop-env-link"
|
||||
<a class="btn stop-env-link"
|
||||
:href="stop_url"
|
||||
data-confirm="Are you sure you want to stop this environment?"
|
||||
data-method="post"
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
(() => {
|
||||
/*
|
||||
* 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 @@
|
|||
}
|
||||
|
||||
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 {
|
||||
$('.js-environment-timeago', $template).remove();
|
||||
environment.name += '.';
|
||||
|
|
|
@ -1,64 +0,0 @@
|
|||
/* 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);
|
|
@ -0,0 +1,73 @@
|
|||
(() => {
|
||||
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;
|
||||
})();
|
|
@ -0,0 +1,130 @@
|
|||
/*
|
||||
* 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 = {}));
|
|
@ -0,0 +1,54 @@
|
|||
//= 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.
|
|||
}
|
||||
const $calendarWrap = this.$parentEl.find('.user-calendar');
|
||||
$calendarWrap.load($calendarWrap.data('href'));
|
||||
new Activities();
|
||||
new gl.Activities();
|
||||
return this.loaded['activity'] = true;
|
||||
}
|
||||
|
||||
|
|
|
@ -138,16 +138,15 @@
|
|||
|
||||
<a v-if="hasRef"
|
||||
class="monospace branch-name"
|
||||
:href="ref.ref_url"
|
||||
v-html="ref.name">
|
||||
:href="ref.ref_url">
|
||||
{{ref.name}}
|
||||
</a>
|
||||
|
||||
<div class="icon-container commit-icon commit-icon-container">
|
||||
</div>
|
||||
<div class="icon-container commit-icon commit-icon-container"></div>
|
||||
|
||||
<a class="commit-id monospace"
|
||||
:href="commit_url"
|
||||
v-html="short_sha">
|
||||
:href="commit_url">
|
||||
{{short_sha}}
|
||||
</a>
|
||||
|
||||
<p class="commit-title">
|
||||
|
@ -156,14 +155,15 @@
|
|||
class="avatar-image-container"
|
||||
:href="author.web_url">
|
||||
<img
|
||||
class="avatar has-tooltip s20"
|
||||
class="avatar has-tooltip s20"
|
||||
:src="author.avatar_url"
|
||||
:alt="userImageAltDescription"
|
||||
:title="author.username" />
|
||||
</a>
|
||||
|
||||
<a class="commit-row-message"
|
||||
:href="commit_url" v-html="title">
|
||||
:href="commit_url">
|
||||
{{title}}
|
||||
</a>
|
||||
</span>
|
||||
<span v-else>
|
||||
|
|
|
@ -254,3 +254,32 @@
|
|||
.content-block-small {
|
||||
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;
|
|||
$provider-btn-group-border: #e5e5e5;
|
||||
$provider-btn-not-active-color: #4688f1;
|
||||
$link-underline-blue: #4a8bee;
|
||||
$active-item-blue: #4a8bee;
|
||||
$layout-link-gray: #7e7c7c;
|
||||
$todo-alert-blue: #428bca;
|
||||
$btn-side-margin: 10px;
|
||||
|
@ -283,6 +284,9 @@ $calendar-unselectable-bg: $gray-light;
|
|||
*/
|
||||
$cycle-analytics-box-padding: 30px;
|
||||
$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
|
||||
|
|
|
@ -1,10 +1,53 @@
|
|||
#cycle-analytics {
|
||||
max-width: 1000px;
|
||||
margin: 24px auto 0;
|
||||
max-width: 800px;
|
||||
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 {
|
||||
padding: 24px 0;
|
||||
border-bottom: none;
|
||||
|
@ -35,23 +78,20 @@
|
|||
}
|
||||
|
||||
&:last-child {
|
||||
text-align: right;
|
||||
|
||||
@media (max-width: $screen-sm-min) {
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
top: 13px;
|
||||
}
|
||||
.js-ca-dropdown {
|
||||
top: $gl-padding-top;
|
||||
}
|
||||
|
||||
.bordered-box {
|
||||
border: 1px solid $border-color;
|
||||
border-radius: $border-radius-default;
|
||||
|
||||
}
|
||||
|
||||
.content-list {
|
||||
|
@ -141,4 +181,302 @@
|
|||
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 @@
|
|||
}
|
||||
|
||||
.progress {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
}
|
||||
}
|
||||
|
@ -30,7 +31,6 @@
|
|||
margin-right: 7px;
|
||||
}
|
||||
|
||||
// Issue title
|
||||
span a {
|
||||
color: $gl-text-color;
|
||||
word-wrap: break-word;
|
||||
|
@ -39,15 +39,66 @@
|
|||
}
|
||||
|
||||
.milestone-summary {
|
||||
margin-bottom: 25px;
|
||||
|
||||
.milestone-stat {
|
||||
white-space: nowrap;
|
||||
margin-right: 10px;
|
||||
|
||||
&.with-drilldown {
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.remaining-days {
|
||||
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,
|
||||
|
@ -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
|
|||
def show
|
||||
@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|
|
||||
format.html
|
||||
format.json { render json: cycle_analytics_json }
|
||||
|
@ -22,23 +26,29 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
|
|||
{ start_date: params[:cycle_analytics][:start_date] }
|
||||
end
|
||||
|
||||
def cycle_analytics_json
|
||||
cycle_analytics_view_data = [[:issue, "Issue", "Time before an issue gets scheduled"],
|
||||
[:plan, "Plan", "Time before an issue starts implementation"],
|
||||
[:code, "Code", "Time until first merge request"],
|
||||
[:test, "Test", "Total test time for all commits/merges"],
|
||||
[:review, "Review", "Time between merge request creation and merge/close"],
|
||||
[:staging, "Staging", "From merge request merge until deploy to production"],
|
||||
[:production, "Production", "From issue creation until deploy to production"]]
|
||||
def generate_cycle_analytics_data
|
||||
stats_values = []
|
||||
|
||||
stats = cycle_analytics_view_data.reduce([]) do |stats, (stage_method, stage_text, stage_description)|
|
||||
cycle_analytics_view_data = [[:issue, "Issue", "Related Issues", "Time before an issue gets scheduled"],
|
||||
[:plan, "Plan", "Related Commits", "Time before an issue starts implementation"],
|
||||
[:code, "Code", "Related Merge Requests", "Time spent coding"],
|
||||
[:test, "Test", "Relative Builds Trigger by Commits", "The time taken to build and test the application"],
|
||||
[: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_legend, stage_description)|
|
||||
value = @cycle_analytics.send(stage_method).presence
|
||||
|
||||
stats_values << value.abs if value
|
||||
|
||||
stats << {
|
||||
title: stage_text,
|
||||
description: stage_description,
|
||||
legend: stage_legend,
|
||||
value: value && !value.zero? ? distance_of_time_in_words(value) : nil
|
||||
}
|
||||
|
||||
stats
|
||||
end
|
||||
|
||||
|
@ -52,9 +62,11 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
|
|||
{ title: "Deploy".pluralize(deploys), value: deploys }
|
||||
]
|
||||
|
||||
{
|
||||
summary: summary,
|
||||
stats: stats
|
||||
cycle_analytics_hash = { summary: summary,
|
||||
stats: stats,
|
||||
permissions: @cycle_analytics.permissions(user: current_user)
|
||||
}
|
||||
|
||||
[stats_values, cycle_analytics_hash]
|
||||
end
|
||||
end
|
||||
|
|
|
@ -82,12 +82,12 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
|
||||
@merge_request_diff =
|
||||
if params[:diff_id]
|
||||
@merge_request.merge_request_diffs.find(params[:diff_id])
|
||||
@merge_request.merge_request_diffs.viewable.find(params[:diff_id])
|
||||
else
|
||||
@merge_request.merge_request_diff
|
||||
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 }
|
||||
|
||||
if params[:start_sha].present?
|
||||
|
@ -417,7 +417,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
|
||||
response = {
|
||||
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,
|
||||
coverage: coverage
|
||||
}
|
||||
|
@ -564,7 +564,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
def define_pipelines_vars
|
||||
@pipelines = @merge_request.all_pipelines
|
||||
|
||||
if @pipelines.present?
|
||||
if @pipelines.present? && @merge_request.commits.present?
|
||||
@pipeline = @pipelines.first
|
||||
@statuses = @pipeline.statuses.relevant
|
||||
end
|
||||
|
|
|
@ -50,14 +50,14 @@ module ApplicationSettingsHelper
|
|||
def restricted_level_checkboxes(help_block_id)
|
||||
Gitlab::VisibilityLevel.options.map do |name, level|
|
||||
checked = restricted_visibility_levels(true).include?(level)
|
||||
css_class = 'btn'
|
||||
css_class += ' active' if checked
|
||||
checkbox_name = 'application_setting[restricted_visibility_levels][]'
|
||||
css_class = checked ? 'active' : ''
|
||||
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,
|
||||
autocomplete: 'off',
|
||||
'aria-describedby' => help_block_id) + name
|
||||
'aria-describedby' => help_block_id,
|
||||
id: name) + visibility_level_icon(level) + name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -48,4 +48,8 @@ module GroupsHelper
|
|||
"#{status.humanize} #{projects_lfs_status(group)}"
|
||||
end
|
||||
end
|
||||
|
||||
def group_issues(group)
|
||||
IssuesFinder.new(current_user, group_id: group.id).execute
|
||||
end
|
||||
end
|
||||
|
|
|
@ -455,4 +455,8 @@ module ProjectsHelper
|
|||
def project_child_container_class(view_path)
|
||||
view_path == "projects/issues/issues" ? "prepend-top-default" : "project-show-#{view_path}"
|
||||
end
|
||||
|
||||
def project_issues(project)
|
||||
IssuesFinder.new(current_user, project_id: project.id).execute
|
||||
end
|
||||
end
|
||||
|
|
|
@ -487,6 +487,10 @@ module Ci
|
|||
]
|
||||
end
|
||||
|
||||
def credentials
|
||||
Gitlab::Ci::Build::Credentials::Factory.new(self).create!
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_artifacts_size
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# == 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.
|
||||
#
|
||||
# Used by Issue, Note, MergeRequest, and Commit.
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
class CycleAnalytics
|
||||
STAGES = %i[issue plan code test review staging production].freeze
|
||||
|
||||
def initialize(project, from:)
|
||||
@project = project
|
||||
@from = from
|
||||
|
@ -9,6 +11,10 @@ class CycleAnalytics
|
|||
@summary ||= Summary.new(@project, from: @from)
|
||||
end
|
||||
|
||||
def permissions(user:)
|
||||
Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project)
|
||||
end
|
||||
|
||||
def issue
|
||||
@fetcher.calculate_metric(:issue,
|
||||
Issue.arel_table[:created_at],
|
||||
|
|
|
@ -19,7 +19,7 @@ class Environment < ActiveRecord::Base
|
|||
allow_nil: 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 :stopped, -> { with_state(:stopped) }
|
||||
|
@ -99,4 +99,12 @@ class Environment < ActiveRecord::Base
|
|||
stop
|
||||
stop_action.play(current_user)
|
||||
end
|
||||
|
||||
def actions_for(environment)
|
||||
return [] unless manual_actions
|
||||
|
||||
manual_actions.select do |action|
|
||||
action.expanded_environment_name == environment
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,6 +11,9 @@ class MergeRequestDiff < ActiveRecord::Base
|
|||
|
||||
belongs_to :merge_request
|
||||
|
||||
serialize :st_commits
|
||||
serialize :st_diffs
|
||||
|
||||
state_machine :state, initial: :empty do
|
||||
state :collected
|
||||
state :overflow
|
||||
|
@ -22,8 +25,7 @@ class MergeRequestDiff < ActiveRecord::Base
|
|||
state :overflow_diff_lines_limit
|
||||
end
|
||||
|
||||
serialize :st_commits
|
||||
serialize :st_diffs
|
||||
scope :viewable, -> { without_state(:empty) }
|
||||
|
||||
# All diff information is collected from repository after object is created.
|
||||
# It allows you to override variables like head_commit_sha before getting diff.
|
||||
|
|
|
@ -1086,7 +1086,7 @@ class Project < ActiveRecord::Base
|
|||
"refs/heads/#{branch}",
|
||||
force: true)
|
||||
repository.copy_gitattributes(branch)
|
||||
repository.expire_avatar_cache(branch)
|
||||
repository.expire_avatar_cache
|
||||
reload_default_branch
|
||||
end
|
||||
|
||||
|
|
|
@ -128,15 +128,9 @@ class JiraService < IssueTrackerService
|
|||
|
||||
return unless jira_issue.present?
|
||||
|
||||
project = self.project
|
||||
noteable_name = noteable.model_name.singular
|
||||
noteable_id = if noteable.is_a?(Commit)
|
||||
noteable.id
|
||||
else
|
||||
noteable.iid
|
||||
end
|
||||
|
||||
entity_url = build_entity_url(noteable_name.to_sym, noteable_id)
|
||||
noteable_id = noteable.respond_to?(:iid) ? noteable.iid : noteable.id
|
||||
noteable_type = noteable_name(noteable)
|
||||
entity_url = build_entity_url(noteable_type, noteable_id)
|
||||
|
||||
data = {
|
||||
user: {
|
||||
|
@ -144,11 +138,11 @@ class JiraService < IssueTrackerService
|
|||
url: resource_url(user_path(author)),
|
||||
},
|
||||
project: {
|
||||
name: project.path_with_namespace,
|
||||
url: resource_url(namespace_project_path(project.namespace, project))
|
||||
name: self.project.path_with_namespace,
|
||||
url: resource_url(namespace_project_path(project.namespace, self.project))
|
||||
},
|
||||
entity: {
|
||||
name: noteable_name.humanize.downcase,
|
||||
name: noteable_type.humanize.downcase,
|
||||
url: entity_url,
|
||||
title: noteable.title
|
||||
}
|
||||
|
@ -285,18 +279,26 @@ class JiraService < IssueTrackerService
|
|||
"#{Settings.gitlab.base_url.chomp("/")}#{resource}"
|
||||
end
|
||||
|
||||
def build_entity_url(entity_name, entity_id)
|
||||
def build_entity_url(noteable_type, entity_id)
|
||||
polymorphic_url(
|
||||
[
|
||||
self.project.namespace.becomes(Namespace),
|
||||
self.project,
|
||||
entity_name
|
||||
noteable_type.to_sym
|
||||
],
|
||||
id: entity_id,
|
||||
host: Settings.gitlab.base_url
|
||||
)
|
||||
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
|
||||
def jira_request
|
||||
yield
|
||||
|
|
|
@ -1,16 +1,54 @@
|
|||
require 'securerandom'
|
||||
|
||||
class Repository
|
||||
class CommitError < StandardError; end
|
||||
|
||||
# Files to use as a project avatar in case no avatar was uploaded via the web
|
||||
# UI.
|
||||
AVATAR_FILES = %w{logo.png logo.jpg logo.gif}
|
||||
|
||||
include Gitlab::ShellAdapter
|
||||
|
||||
attr_accessor :path_with_namespace, :project
|
||||
|
||||
class CommitError < StandardError; end
|
||||
|
||||
# Methods that cache data from the Git repository.
|
||||
#
|
||||
# Each entry in this Array should have a corresponding method with the exact
|
||||
# same name. The cache key used by those methods must also match method's
|
||||
# name.
|
||||
#
|
||||
# For example, for entry `:readme` there's a method called `readme` which
|
||||
# stores its data in the `readme` cache key.
|
||||
CACHED_METHODS = %i(size commit_count readme version contribution_guide
|
||||
changelog license_blob license_key gitignore koding_yml
|
||||
gitlab_ci_yml branch_names tag_names branch_count
|
||||
tag_count avatar exists? empty? root_ref)
|
||||
|
||||
# Certain method caches should be refreshed when certain types of files are
|
||||
# changed. This Hash maps file types (as returned by Gitlab::FileDetector) to
|
||||
# the corresponding methods to call for refreshing caches.
|
||||
METHOD_CACHES_FOR_FILE_TYPES = {
|
||||
readme: :readme,
|
||||
changelog: :changelog,
|
||||
license: %i(license_blob license_key),
|
||||
contributing: :contribution_guide,
|
||||
version: :version,
|
||||
gitignore: :gitignore,
|
||||
koding: :koding_yml,
|
||||
gitlab_ci: :gitlab_ci_yml,
|
||||
avatar: :avatar
|
||||
}
|
||||
|
||||
# Wraps around the given method and caches its output in Redis and an instance
|
||||
# variable.
|
||||
#
|
||||
# This only works for methods that do not take any arguments.
|
||||
def self.cache_method(name, fallback: nil)
|
||||
original = :"_uncached_#{name}"
|
||||
|
||||
alias_method(original, name)
|
||||
|
||||
define_method(name) do
|
||||
cache_method_output(name, fallback: fallback) { __send__(original) }
|
||||
end
|
||||
end
|
||||
|
||||
def self.storages
|
||||
Gitlab.config.repositories.storages
|
||||
end
|
||||
|
@ -37,24 +75,6 @@ class Repository
|
|||
)
|
||||
end
|
||||
|
||||
def exists?
|
||||
return @exists unless @exists.nil?
|
||||
|
||||
@exists = cache.fetch(:exists?) do
|
||||
begin
|
||||
raw_repository && raw_repository.rugged ? true : false
|
||||
rescue Gitlab::Git::Repository::NoRepository
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def empty?
|
||||
return @empty unless @empty.nil?
|
||||
|
||||
@empty = cache.fetch(:empty?) { raw_repository.empty? }
|
||||
end
|
||||
|
||||
#
|
||||
# Git repository can contains some hidden refs like:
|
||||
# /refs/notes/*
|
||||
|
@ -221,10 +241,6 @@ class Repository
|
|||
branch_names + tag_names
|
||||
end
|
||||
|
||||
def branch_names
|
||||
@branch_names ||= cache.fetch(:branch_names) { branches.map(&:name) }
|
||||
end
|
||||
|
||||
def branch_exists?(branch_name)
|
||||
branch_names.include?(branch_name)
|
||||
end
|
||||
|
@ -274,34 +290,6 @@ class Repository
|
|||
ref_exists?(keep_around_ref_name(sha))
|
||||
end
|
||||
|
||||
def tag_names
|
||||
cache.fetch(:tag_names) { raw_repository.tag_names }
|
||||
end
|
||||
|
||||
def commit_count
|
||||
cache.fetch(:commit_count) do
|
||||
begin
|
||||
raw_repository.commit_count(self.root_ref)
|
||||
rescue
|
||||
0
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def branch_count
|
||||
@branch_count ||= cache.fetch(:branch_count) { branches.size }
|
||||
end
|
||||
|
||||
def tag_count
|
||||
@tag_count ||= cache.fetch(:tag_count) { raw_repository.rugged.tags.count }
|
||||
end
|
||||
|
||||
# Return repo size in megabytes
|
||||
# Cached in redis
|
||||
def size
|
||||
cache.fetch(:size) { raw_repository.size }
|
||||
end
|
||||
|
||||
def diverging_commit_counts(branch)
|
||||
root_ref_hash = raw_repository.rev_parse_target(root_ref).oid
|
||||
cache.fetch(:"diverging_commit_counts_#{branch.name}") do
|
||||
|
@ -317,48 +305,55 @@ class Repository
|
|||
end
|
||||
end
|
||||
|
||||
# Keys for data that can be affected for any commit push.
|
||||
def cache_keys
|
||||
%i(size commit_count
|
||||
readme version contribution_guide changelog
|
||||
license_blob license_key gitignore koding_yml)
|
||||
end
|
||||
|
||||
# Keys for data on branch/tag operations.
|
||||
def cache_keys_for_branches_and_tags
|
||||
%i(branch_names tag_names branch_count tag_count)
|
||||
end
|
||||
|
||||
def build_cache
|
||||
(cache_keys + cache_keys_for_branches_and_tags).each do |key|
|
||||
unless cache.exist?(key)
|
||||
send(key)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def expire_tags_cache
|
||||
cache.expire(:tag_names)
|
||||
expire_method_caches(%i(tag_names tag_count))
|
||||
@tags = nil
|
||||
end
|
||||
|
||||
def expire_branches_cache
|
||||
cache.expire(:branch_names)
|
||||
@branch_names = nil
|
||||
expire_method_caches(%i(branch_names branch_count))
|
||||
@local_branches = nil
|
||||
end
|
||||
|
||||
def expire_cache(branch_name = nil, revision = nil)
|
||||
cache_keys.each do |key|
|
||||
def expire_statistics_caches
|
||||
expire_method_caches(%i(size commit_count))
|
||||
end
|
||||
|
||||
def expire_all_method_caches
|
||||
expire_method_caches(CACHED_METHODS)
|
||||
end
|
||||
|
||||
# Expires the caches of a specific set of methods
|
||||
def expire_method_caches(methods)
|
||||
methods.each do |key|
|
||||
cache.expire(key)
|
||||
|
||||
ivar = cache_instance_variable_name(key)
|
||||
|
||||
remove_instance_variable(ivar) if instance_variable_defined?(ivar)
|
||||
end
|
||||
end
|
||||
|
||||
def expire_avatar_cache
|
||||
expire_method_caches(%i(avatar))
|
||||
end
|
||||
|
||||
# Refreshes the method caches of this repository.
|
||||
#
|
||||
# types - An Array of file types (e.g. `:readme`) used to refresh extra
|
||||
# caches.
|
||||
def refresh_method_caches(types)
|
||||
to_refresh = []
|
||||
|
||||
types.each do |type|
|
||||
methods = METHOD_CACHES_FOR_FILE_TYPES[type.to_sym]
|
||||
|
||||
to_refresh.concat(Array(methods)) if methods
|
||||
end
|
||||
|
||||
expire_branch_cache(branch_name)
|
||||
expire_avatar_cache(branch_name, revision)
|
||||
expire_method_caches(to_refresh)
|
||||
|
||||
# This ensures this particular cache is flushed after the first commit to a
|
||||
# new repository.
|
||||
expire_emptiness_caches if empty?
|
||||
to_refresh.each { |method| send(method) }
|
||||
end
|
||||
|
||||
def expire_branch_cache(branch_name = nil)
|
||||
|
@ -377,15 +372,14 @@ class Repository
|
|||
end
|
||||
|
||||
def expire_root_ref_cache
|
||||
cache.expire(:root_ref)
|
||||
@root_ref = nil
|
||||
expire_method_caches(%i(root_ref))
|
||||
end
|
||||
|
||||
# Expires the cache(s) used to determine if a repository is empty or not.
|
||||
def expire_emptiness_caches
|
||||
cache.expire(:empty?)
|
||||
@empty = nil
|
||||
return unless empty?
|
||||
|
||||
expire_method_caches(%i(empty?))
|
||||
expire_has_visible_content_cache
|
||||
end
|
||||
|
||||
|
@ -394,51 +388,22 @@ class Repository
|
|||
@has_visible_content = nil
|
||||
end
|
||||
|
||||
def expire_branch_count_cache
|
||||
cache.expire(:branch_count)
|
||||
@branch_count = nil
|
||||
end
|
||||
|
||||
def expire_tag_count_cache
|
||||
cache.expire(:tag_count)
|
||||
@tag_count = nil
|
||||
end
|
||||
|
||||
def lookup_cache
|
||||
@lookup_cache ||= {}
|
||||
end
|
||||
|
||||
def expire_avatar_cache(branch_name = nil, revision = nil)
|
||||
# Avatars are pulled from the default branch, thus if somebody pushes to a
|
||||
# different branch there's no need to expire anything.
|
||||
return if branch_name && branch_name != root_ref
|
||||
|
||||
# We don't want to flush the cache if the commit didn't actually make any
|
||||
# changes to any of the possible avatar files.
|
||||
if revision && commit = self.commit(revision)
|
||||
return unless commit.raw_diffs(deltas_only: true).
|
||||
any? { |diff| AVATAR_FILES.include?(diff.new_path) }
|
||||
end
|
||||
|
||||
cache.expire(:avatar)
|
||||
|
||||
@avatar = nil
|
||||
end
|
||||
|
||||
def expire_exists_cache
|
||||
cache.expire(:exists?)
|
||||
@exists = nil
|
||||
expire_method_caches(%i(exists?))
|
||||
end
|
||||
|
||||
# expire cache that doesn't depend on repository data (when expiring)
|
||||
def expire_content_cache
|
||||
expire_tags_cache
|
||||
expire_tag_count_cache
|
||||
expire_branches_cache
|
||||
expire_branch_count_cache
|
||||
expire_root_ref_cache
|
||||
expire_emptiness_caches
|
||||
expire_exists_cache
|
||||
expire_statistics_caches
|
||||
end
|
||||
|
||||
# Runs code after a repository has been created.
|
||||
|
@ -453,9 +418,8 @@ class Repository
|
|||
# Runs code just before a repository is deleted.
|
||||
def before_delete
|
||||
expire_exists_cache
|
||||
|
||||
expire_cache if exists?
|
||||
|
||||
expire_all_method_caches
|
||||
expire_branch_cache if exists?
|
||||
expire_content_cache
|
||||
|
||||
repository_event(:remove_repository)
|
||||
|
@ -472,9 +436,9 @@ class Repository
|
|||
|
||||
# Runs code before pushing (= creating or removing) a tag.
|
||||
def before_push_tag
|
||||
expire_cache
|
||||
expire_statistics_caches
|
||||
expire_emptiness_caches
|
||||
expire_tags_cache
|
||||
expire_tag_count_cache
|
||||
|
||||
repository_event(:push_tag)
|
||||
end
|
||||
|
@ -482,7 +446,7 @@ class Repository
|
|||
# Runs code before removing a tag.
|
||||
def before_remove_tag
|
||||
expire_tags_cache
|
||||
expire_tag_count_cache
|
||||
expire_statistics_caches
|
||||
|
||||
repository_event(:remove_tag)
|
||||
end
|
||||
|
@ -494,12 +458,14 @@ class Repository
|
|||
# Runs code after a repository has been forked/imported.
|
||||
def after_import
|
||||
expire_content_cache
|
||||
build_cache
|
||||
expire_tags_cache
|
||||
expire_branches_cache
|
||||
end
|
||||
|
||||
# Runs code after a new commit has been pushed.
|
||||
def after_push_commit(branch_name, revision)
|
||||
expire_cache(branch_name, revision)
|
||||
def after_push_commit(branch_name)
|
||||
expire_statistics_caches
|
||||
expire_branch_cache(branch_name)
|
||||
|
||||
repository_event(:push_commit, branch: branch_name)
|
||||
end
|
||||
|
@ -508,7 +474,6 @@ class Repository
|
|||
def after_create_branch
|
||||
expire_branches_cache
|
||||
expire_has_visible_content_cache
|
||||
expire_branch_count_cache
|
||||
|
||||
repository_event(:push_branch)
|
||||
end
|
||||
|
@ -523,7 +488,6 @@ class Repository
|
|||
# Runs code after an existing branch has been removed.
|
||||
def after_remove_branch
|
||||
expire_has_visible_content_cache
|
||||
expire_branch_count_cache
|
||||
expire_branches_cache
|
||||
end
|
||||
|
||||
|
@ -550,86 +514,127 @@ class Repository
|
|||
Gitlab::Git::Blob.raw(self, oid)
|
||||
end
|
||||
|
||||
def readme
|
||||
cache.fetch(:readme) { tree(:head).readme }
|
||||
def root_ref
|
||||
if raw_repository
|
||||
raw_repository.root_ref
|
||||
else
|
||||
# When the repo does not exist we raise this error so no data is cached.
|
||||
raise Rugged::ReferenceError
|
||||
end
|
||||
end
|
||||
cache_method :root_ref
|
||||
|
||||
def exists?
|
||||
refs_directory_exists?
|
||||
end
|
||||
cache_method :exists?
|
||||
|
||||
def empty?
|
||||
raw_repository.empty?
|
||||
end
|
||||
cache_method :empty?
|
||||
|
||||
# The size of this repository in megabytes.
|
||||
def size
|
||||
exists? ? raw_repository.size : 0.0
|
||||
end
|
||||
cache_method :size, fallback: 0.0
|
||||
|
||||
def commit_count
|
||||
root_ref ? raw_repository.commit_count(root_ref) : 0
|
||||
end
|
||||
cache_method :commit_count, fallback: 0
|
||||
|
||||
def branch_names
|
||||
branches.map(&:name)
|
||||
end
|
||||
cache_method :branch_names, fallback: []
|
||||
|
||||
def tag_names
|
||||
raw_repository.tag_names
|
||||
end
|
||||
cache_method :tag_names, fallback: []
|
||||
|
||||
def branch_count
|
||||
branches.size
|
||||
end
|
||||
cache_method :branch_count, fallback: 0
|
||||
|
||||
def tag_count
|
||||
raw_repository.rugged.tags.count
|
||||
end
|
||||
cache_method :tag_count, fallback: 0
|
||||
|
||||
def avatar
|
||||
if tree = file_on_head(:avatar)
|
||||
tree.path
|
||||
end
|
||||
end
|
||||
cache_method :avatar
|
||||
|
||||
def readme
|
||||
if head = tree(:head)
|
||||
head.readme
|
||||
end
|
||||
end
|
||||
cache_method :readme
|
||||
|
||||
def version
|
||||
cache.fetch(:version) do
|
||||
tree(:head).blobs.find do |file|
|
||||
file.name.casecmp('version').zero?
|
||||
end
|
||||
end
|
||||
file_on_head(:version)
|
||||
end
|
||||
cache_method :version
|
||||
|
||||
def contribution_guide
|
||||
cache.fetch(:contribution_guide) do
|
||||
tree(:head).blobs.find do |file|
|
||||
file.contributing?
|
||||
end
|
||||
end
|
||||
file_on_head(:contributing)
|
||||
end
|
||||
cache_method :contribution_guide
|
||||
|
||||
def changelog
|
||||
cache.fetch(:changelog) do
|
||||
file_on_head(/\A(changelog|history|changes|news)/i)
|
||||
end
|
||||
file_on_head(:changelog)
|
||||
end
|
||||
cache_method :changelog
|
||||
|
||||
def license_blob
|
||||
return nil unless head_exists?
|
||||
|
||||
cache.fetch(:license_blob) do
|
||||
file_on_head(/\A(licen[sc]e|copying)(\..+|\z)/i)
|
||||
end
|
||||
file_on_head(:license)
|
||||
end
|
||||
cache_method :license_blob
|
||||
|
||||
def license_key
|
||||
return nil unless head_exists?
|
||||
return unless exists?
|
||||
|
||||
cache.fetch(:license_key) do
|
||||
Licensee.license(path).try(:key)
|
||||
end
|
||||
Licensee.license(path).try(:key)
|
||||
end
|
||||
cache_method :license_key
|
||||
|
||||
def gitignore
|
||||
return nil if !exists? || empty?
|
||||
|
||||
cache.fetch(:gitignore) do
|
||||
file_on_head(/\A\.gitignore\z/)
|
||||
end
|
||||
file_on_head(:gitignore)
|
||||
end
|
||||
cache_method :gitignore
|
||||
|
||||
def koding_yml
|
||||
return nil unless head_exists?
|
||||
|
||||
cache.fetch(:koding_yml) do
|
||||
file_on_head(/\A\.koding\.yml\z/)
|
||||
end
|
||||
file_on_head(:koding)
|
||||
end
|
||||
cache_method :koding_yml
|
||||
|
||||
def gitlab_ci_yml
|
||||
return nil unless head_exists?
|
||||
|
||||
@gitlab_ci_yml ||= tree(:head).blobs.find do |file|
|
||||
file.name == '.gitlab-ci.yml'
|
||||
end
|
||||
rescue Rugged::ReferenceError
|
||||
# For unknow reason spinach scenario "Scenario: I change project path"
|
||||
# lead to "Reference 'HEAD' not found" exception from Repository#empty?
|
||||
nil
|
||||
file_on_head(:gitlab_ci)
|
||||
end
|
||||
cache_method :gitlab_ci_yml
|
||||
|
||||
def head_commit
|
||||
@head_commit ||= commit(self.root_ref)
|
||||
end
|
||||
|
||||
def head_tree
|
||||
@head_tree ||= Tree.new(self, head_commit.sha, nil)
|
||||
if head_commit
|
||||
@head_tree ||= Tree.new(self, head_commit.sha, nil)
|
||||
end
|
||||
end
|
||||
|
||||
def tree(sha = :head, path = nil, recursive: false)
|
||||
if sha == :head
|
||||
return unless head_commit
|
||||
|
||||
if path.nil?
|
||||
return head_tree
|
||||
else
|
||||
|
@ -779,10 +784,6 @@ class Repository
|
|||
@tags ||= raw_repository.tags
|
||||
end
|
||||
|
||||
def root_ref
|
||||
@root_ref ||= cache.fetch(:root_ref) { raw_repository.root_ref }
|
||||
end
|
||||
|
||||
def commit_dir(user, path, message, branch, author_email: nil, author_name: nil)
|
||||
update_branch_with_hooks(user, branch) do |ref|
|
||||
options = {
|
||||
|
@ -1140,30 +1141,57 @@ class Repository
|
|||
end
|
||||
end
|
||||
|
||||
def avatar
|
||||
return nil unless exists?
|
||||
# Caches the supplied block both in a cache and in an instance variable.
|
||||
#
|
||||
# The cache key and instance variable are named the same way as the value of
|
||||
# the `key` argument.
|
||||
#
|
||||
# This method will return `nil` if the corresponding instance variable is also
|
||||
# set to `nil`. This ensures we don't keep yielding the block when it returns
|
||||
# `nil`.
|
||||
#
|
||||
# key - The name of the key to cache the data in.
|
||||
# fallback - A value to fall back to in the event of a Git error.
|
||||
def cache_method_output(key, fallback: nil, &block)
|
||||
ivar = cache_instance_variable_name(key)
|
||||
|
||||
@avatar ||= cache.fetch(:avatar) do
|
||||
AVATAR_FILES.find do |file|
|
||||
blob_at_branch(root_ref, file)
|
||||
if instance_variable_defined?(ivar)
|
||||
instance_variable_get(ivar)
|
||||
else
|
||||
begin
|
||||
instance_variable_set(ivar, cache.fetch(key, &block))
|
||||
rescue Rugged::ReferenceError, Gitlab::Git::Repository::NoRepository
|
||||
# if e.g. HEAD or the entire repository doesn't exist we want to
|
||||
# gracefully handle this and not cache anything.
|
||||
fallback
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def cache_instance_variable_name(key)
|
||||
:"@#{key.to_s.tr('?!', '')}"
|
||||
end
|
||||
|
||||
def file_on_head(type)
|
||||
if head = tree(:head)
|
||||
head.blobs.find do |file|
|
||||
Gitlab::FileDetector.type_of(file.name) == type
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def refs_directory_exists?
|
||||
return false unless path_with_namespace
|
||||
|
||||
File.exist?(File.join(path_to_repo, 'refs'))
|
||||
end
|
||||
|
||||
def cache
|
||||
@cache ||= RepositoryCache.new(path_with_namespace, @project.id)
|
||||
end
|
||||
|
||||
def head_exists?
|
||||
exists? && !empty? && !rugged.head_unborn?
|
||||
end
|
||||
|
||||
def file_on_head(regex)
|
||||
tree(:head).blobs.find { |file| file.name =~ regex }
|
||||
end
|
||||
|
||||
def tags_sorted_by_committed_date
|
||||
tags.sort_by { |tag| tag.dereferenced_target.committed_date }
|
||||
end
|
||||
|
|
|
@ -6,6 +6,7 @@ class Snippet < ActiveRecord::Base
|
|||
include Referable
|
||||
include Sortable
|
||||
include Awardable
|
||||
include Mentionable
|
||||
|
||||
cache_markdown_field :title, pipeline: :single_line
|
||||
cache_markdown_field :content
|
||||
|
|
|
@ -18,7 +18,9 @@ class Tree
|
|||
def 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?(blob.name)
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
require_relative 'base_service'
|
||||
|
||||
##
|
||||
# Branch can be deleted either by DeleteBranchService
|
||||
# or by GitPushService.
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
require_relative 'base_service'
|
||||
|
||||
class CreateBranchService < BaseService
|
||||
def execute(branch_name, ref, source_project: @project)
|
||||
valid_branch = Gitlab::GitRefValidator.validate(branch_name)
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
require_relative 'base_service'
|
||||
|
||||
class CreateDeploymentService < BaseService
|
||||
def execute(deployable = nil)
|
||||
return unless executable?
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
require_relative 'base_service'
|
||||
|
||||
class CreateReleaseService < BaseService
|
||||
def execute(tag_name, release_description)
|
||||
repository = project.repository
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
require_relative 'base_service'
|
||||
|
||||
class CreateTagService < BaseService
|
||||
def execute(tag_name, target, message, release_description = nil)
|
||||
valid_tag = Gitlab::GitRefValidator.validate(tag_name)
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
require_relative 'base_service'
|
||||
|
||||
class DeleteBranchService < BaseService
|
||||
def execute(branch_name)
|
||||
repository = project.repository
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
require_relative 'base_service'
|
||||
|
||||
class DeleteMergedBranchesService < BaseService
|
||||
def async_execute
|
||||
DeleteMergedBranchesWorker.perform_async(project.id, current_user.id)
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
require_relative 'base_service'
|
||||
|
||||
class DeleteTagService < BaseService
|
||||
def execute(tag_name)
|
||||
repository = project.repository
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
require_relative "base_service"
|
||||
|
||||
module Files
|
||||
class CreateDirService < Files::BaseService
|
||||
def commit
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
require_relative "base_service"
|
||||
|
||||
module Files
|
||||
class CreateService < Files::BaseService
|
||||
def commit
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
require_relative "base_service"
|
||||
|
||||
module Files
|
||||
class DeleteService < Files::BaseService
|
||||
def commit
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
require_relative "base_service"
|
||||
|
||||
module Files
|
||||
class MultiService < Files::BaseService
|
||||
class FileChangedError < StandardError; end
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
require_relative "base_service"
|
||||
|
||||
module Files
|
||||
class UpdateService < Files::BaseService
|
||||
class FileChangedError < StandardError; end
|
||||
|
|
|
@ -18,7 +18,7 @@ class GitPushService < BaseService
|
|||
#
|
||||
def execute
|
||||
@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?
|
||||
@project.repository.after_remove_branch
|
||||
|
@ -51,12 +51,32 @@ class GitPushService < BaseService
|
|||
|
||||
execute_related_hooks
|
||||
perform_housekeeping
|
||||
|
||||
update_caches
|
||||
end
|
||||
|
||||
def update_gitattributes
|
||||
@project.repository.copy_gitattributes(params[:ref])
|
||||
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
|
||||
|
||||
def execute_related_hooks
|
||||
|
@ -70,7 +90,6 @@ class GitPushService < BaseService
|
|||
@project.execute_hooks(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
|
||||
ProjectCacheWorker.perform_async(@project.id)
|
||||
|
||||
if push_remove_branch?
|
||||
AfterBranchDeleteService
|
||||
|
|
|
@ -60,7 +60,15 @@ module MergeRequests
|
|||
merge_requests = filter_merge_requests(merge_requests)
|
||||
|
||||
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
|
||||
end
|
||||
end
|
||||
|
@ -165,16 +173,5 @@ module MergeRequests
|
|||
def branch_removed?
|
||||
Gitlab::Git.blank_ref?(@newrev)
|
||||
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
|
||||
|
|
|
@ -1,7 +1,3 @@
|
|||
require_relative 'base_service'
|
||||
require_relative 'reopen_service'
|
||||
require_relative 'close_service'
|
||||
|
||||
module MergeRequests
|
||||
class UpdateService < MergeRequests::BaseService
|
||||
def execute(merge_request)
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
require_relative 'base_service'
|
||||
|
||||
class UpdateReleaseService < BaseService
|
||||
def execute(tag_name, release_description)
|
||||
repository = project.repository
|
||||
|
|
|
@ -22,9 +22,8 @@
|
|||
.form-group
|
||||
= f.label :restricted_visibility_levels, class: 'control-label col-sm-2'
|
||||
.col-sm-10
|
||||
- data_attrs = { toggle: 'buttons' }
|
||||
.btn-group{ data: data_attrs }
|
||||
- restricted_level_checkboxes('restricted-visibility-help').each do |level|
|
||||
- restricted_level_checkboxes('restricted-visibility-help').each do |level|
|
||||
.checkbox
|
||||
= level
|
||||
%span.help-block#restricted-visibility-help
|
||||
Selected levels cannot be used by non-admin users for projects or snippets.
|
||||
|
|
|
@ -3,24 +3,27 @@
|
|||
- if current_user
|
||||
= auto_discovery_link_tag(:atom, url_for(params.merge(format: :atom, private_token: current_user.private_token)), title: "#{@group.name} issues")
|
||||
|
||||
.top-area
|
||||
= render 'shared/issuable/nav', type: :issues
|
||||
.nav-controls
|
||||
- if group_issues(@group).exists?
|
||||
.top-area
|
||||
= render 'shared/issuable/nav', type: :issues
|
||||
.nav-controls
|
||||
- if current_user
|
||||
= link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do
|
||||
= icon('rss')
|
||||
%span.icon-label
|
||||
Subscribe
|
||||
= render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
|
||||
|
||||
= render 'shared/issuable/filter', type: :issues
|
||||
|
||||
.row-content-block.second-block
|
||||
Only issues from the
|
||||
%strong #{@group.name}
|
||||
group are listed here.
|
||||
- if current_user
|
||||
= link_to url_for(params.merge(format: :atom, private_token: current_user.private_token)), class: 'btn' do
|
||||
= icon('rss')
|
||||
%span.icon-label
|
||||
Subscribe
|
||||
= render 'shared/new_project_item_select', path: 'issues/new', label: "New Issue"
|
||||
To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page.
|
||||
|
||||
= render 'shared/issuable/filter', type: :issues
|
||||
|
||||
.row-content-block.second-block
|
||||
Only issues from
|
||||
%strong #{@group.name}
|
||||
group are listed here.
|
||||
- if current_user
|
||||
To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page.
|
||||
|
||||
.prepend-top-default
|
||||
= render 'shared/issues'
|
||||
.prepend-top-default
|
||||
= render 'shared/issues'
|
||||
- else
|
||||
= render 'shared/empty_states/issues', project_select_button: true
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
= chat_name.chat_name
|
||||
%td
|
||||
- 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
|
||||
Never
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
= spinner
|
||||
|
||||
:javascript
|
||||
var activity = new Activities();
|
||||
var activity = new gl.Activities();
|
||||
$(document).on('page:restore', function (event) {
|
||||
activity.reloadActivities()
|
||||
})
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
.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}}
|
|
@ -0,0 +1,7 @@
|
|||
.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.
|
|
@ -0,0 +1,15 @@
|
|||
.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')
|
|
@ -1,40 +1,35 @@
|
|||
- @no_container = true
|
||||
- page_title "Cycle Analytics"
|
||||
|
||||
- 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"
|
||||
|
||||
#cycle-analytics{class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) }}
|
||||
|
||||
.bordered-box.landing.content-block{"v-if" => "!isHelpDismissed"}
|
||||
= icon('times', class: 'dismiss-icon', "@click" => "dismissLanding()")
|
||||
.row
|
||||
.col-sm-3.col-xs-12.svg-container
|
||||
= custom_icon('icon_cycle_analytics_splash')
|
||||
.col-sm-8.col-xs-12.inner-content
|
||||
%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.
|
||||
|
||||
= link_to "Read more", help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn'
|
||||
#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" => "!isOverviewDialogDismissed"}
|
||||
= icon("times", class: "dismiss-icon", "@click" => "dismissOverviewDialog()")
|
||||
.row
|
||||
.col-sm-3.col-xs-12.svg-container
|
||||
= custom_icon('icon_cycle_analytics_splash')
|
||||
.col-sm-8.col-xs-12.inner-content
|
||||
%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.
|
||||
|
||||
= link_to "Read more", help_page_path('user/project/cycle_analytics'), target: '_blank', class: 'btn'
|
||||
= icon("spinner spin", "v-show" => "isLoading")
|
||||
|
||||
.wrapper{"v-show" => "!isLoading && !hasError"}
|
||||
.panel.panel-default
|
||||
.panel-heading
|
||||
Pipeline Health
|
||||
|
||||
.content-block
|
||||
.container-fluid
|
||||
.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}}
|
||||
%p.text {{item.title}}
|
||||
|
||||
.col-sm-3.col-xs-12.column
|
||||
.dropdown.inline.js-ca-dropdown
|
||||
%button.dropdown-menu-toggle{"data-toggle" => "dropdown", :type => "button"}
|
||||
|
@ -42,22 +37,54 @@
|
|||
%i.fa.fa-chevron-down
|
||||
%ul.dropdown-menu.dropdown-menu-align-right
|
||||
%li
|
||||
%a{'href' => "#", 'data-value' => '30'}
|
||||
%a{ "href" => "#", "data-value" => "30" }
|
||||
Last 30 days
|
||||
%li
|
||||
%a{'href' => "#", 'data-value' => '90'}
|
||||
%a{ "href" => "#", "data-value" => "90" }
|
||||
Last 90 days
|
||||
|
||||
.bordered-box
|
||||
%ul.content-list
|
||||
%li{"v-for" => "item in analytics.stats"}
|
||||
.container-fluid
|
||||
.row
|
||||
.col-xs-8.title-col
|
||||
%p.title
|
||||
{{item.title}}
|
||||
%p.text
|
||||
{{item.description}}
|
||||
.col-xs-4.value-col
|
||||
%span
|
||||
{{item.value}}
|
||||
.stage-panel-container
|
||||
.panel.panel-default.stage-panel
|
||||
.panel-heading
|
||||
%nav.col-headers
|
||||
%ul
|
||||
%li.stage-header
|
||||
%span.stage-name
|
||||
Stage
|
||||
%i.has-tooltip.fa.fa-question-circle{ "data-placement" => "top", title: "The phase of the development lifecycle.", "aria-hidden" => "true" }
|
||||
%li.median-header
|
||||
%span.stage-name
|
||||
Median
|
||||
%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" }
|
||||
%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" }
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
%ul.content-list.issues-list.issuable-list
|
||||
= render partial: "projects/issues/issue", collection: @issues
|
||||
- if @issues.blank?
|
||||
%li
|
||||
.nothing-here-block No issues to show
|
||||
= render 'shared/empty_states/issues'
|
||||
|
||||
- if @issues.present?
|
||||
= paginate @issues, theme: "gitlab"
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
- if current_user
|
||||
= 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.any?
|
||||
- if project_issues(@project).exists?
|
||||
%div{ class: (container_class) }
|
||||
.top-area
|
||||
= render 'shared/issuable/nav', type: :issues
|
||||
.nav-controls
|
||||
|
@ -36,21 +36,5 @@
|
|||
= render 'issues'
|
||||
- if new_issue_email
|
||||
= render 'issue_by_email', email: new_issue_email
|
||||
- else
|
||||
.blank-state.blank-state-welcome
|
||||
%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
|
||||
- else
|
||||
= render 'shared/empty_states/issues', button_path: new_namespace_project_issue_path(@project.namespace, @project)
|
||||
|
|
|
@ -9,10 +9,10 @@
|
|||
|
||||
- if @project.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?
|
||||
= render 'projects/merge_requests/widget/open/missing_branch'
|
||||
- elsif @merge_request.commits.blank?
|
||||
= render 'projects/merge_requests/widget/open/nothing'
|
||||
- elsif @merge_request.unchecked?
|
||||
= render 'projects/merge_requests/widget/open/check'
|
||||
- elsif @merge_request.cannot_be_merged? && !resolved_conflicts
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
= render "projects/issues/head"
|
||||
|
||||
%div{ class: container_class }
|
||||
.detail-page-header
|
||||
.detail-page-header.milestone-page-header
|
||||
.status-box{ class: status_box_class(@milestone) }
|
||||
- if @milestone.closed?
|
||||
Closed
|
||||
|
@ -12,13 +12,14 @@
|
|||
Past due
|
||||
- else
|
||||
Open
|
||||
%span.identifier
|
||||
Milestone ##{@milestone.iid}
|
||||
- if @milestone.expires_at
|
||||
%span.creator
|
||||
·
|
||||
= @milestone.expires_at
|
||||
.pull-right
|
||||
.header-text-content
|
||||
%span.identifier
|
||||
Milestone ##{@milestone.iid}
|
||||
- if @milestone.expires_at
|
||||
%span.creator
|
||||
·
|
||||
= @milestone.expires_at
|
||||
.milestone-buttons
|
||||
- if can?(current_user, :admin_milestone, @project)
|
||||
- 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"
|
||||
|
|
|
@ -13,4 +13,4 @@
|
|||
= render 'projects/issues/issue', issue: issue
|
||||
= paginate @issues, theme: "gitlab"
|
||||
- else
|
||||
.nothing-here-block No issues to show
|
||||
= render 'shared/empty_states/issues'
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
- 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.
|
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 16 KiB |
|
@ -0,0 +1,3 @@
|
|||
<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>
|
After Width: | Height: | Size: 549 B |
|
@ -0,0 +1,81 @@
|
|||
<svg width="366px" height="229px" viewBox="784 258 366 229" 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="35" y="39" width="24" height="21" rx="10"></rect>
|
||||
<mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="24" height="21" fill="white">
|
||||
<use xlink:href="#path-1"></use>
|
||||
</mask>
|
||||
<rect id="path-3" x="64.8662386" y="58.3882666" width="10" height="71" rx="5"></rect>
|
||||
<mask id="mask-4" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="10" height="71" fill="white">
|
||||
<use xlink:href="#path-3"></use>
|
||||
</mask>
|
||||
<rect id="path-5" x="18.1550472" y="58.3882666" width="10" height="71" rx="5"></rect>
|
||||
<mask id="mask-6" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="10" height="71" fill="white">
|
||||
<use xlink:href="#path-5"></use>
|
||||
</mask>
|
||||
<rect id="path-7" x="24" y="56" width="46" height="10" rx="5"></rect>
|
||||
<mask id="mask-8" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="46" height="10" fill="white">
|
||||
<use xlink:href="#path-7"></use>
|
||||
</mask>
|
||||
<rect id="path-9" x="42" y="60" width="10" height="68" rx="5"></rect>
|
||||
<mask id="mask-10" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="10" height="68" fill="white">
|
||||
<use xlink:href="#path-9"></use>
|
||||
</mask>
|
||||
<rect id="path-11" x="69" y="12" width="12" height="12" rx="3"></rect>
|
||||
<mask id="mask-12" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="12" height="12" fill="white">
|
||||
<use xlink:href="#path-11"></use>
|
||||
</mask>
|
||||
<rect id="path-13" x="40" y="18" width="14" height="22" rx="6"></rect>
|
||||
<mask id="mask-14" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="14" height="22" fill="white">
|
||||
<use xlink:href="#path-13"></use>
|
||||
</mask>
|
||||
<rect id="path-15" x="41" y="8" width="34" height="20" rx="3"></rect>
|
||||
<mask id="mask-16" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="34" height="20" fill="white">
|
||||
<use xlink:href="#path-15"></use>
|
||||
</mask>
|
||||
<path d="M8,8.00793008 C8,6.34669617 9.34984627,5.0321392 11.0036812,5.07151622 L46.9963188,5.92848378 C48.6552061,5.9679811 50,7.34177063 50,8.99109042 L50,27.0089096 C50,28.6608432 48.6501537,30.0321392 46.9963188,30.0715162 L11.0036812,30.9284838 C9.34479389,30.9679811 8,29.6568766 8,27.9920699 L8,8.00793008 Z" id="path-17"></path>
|
||||
<mask id="mask-18" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="42" height="25.858699" fill="white">
|
||||
<use xlink:href="#path-17"></use>
|
||||
</mask>
|
||||
<rect id="path-19" x="-7.10542736e-15" y="1.77635684e-14" width="16" height="36" rx="3"></rect>
|
||||
<mask id="mask-20" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="16" height="36" fill="white">
|
||||
<use xlink:href="#path-19"></use>
|
||||
</mask>
|
||||
</defs>
|
||||
<g id="Group-7" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd" transform="translate(786.000000, 259.000000)">
|
||||
<g id="Group-5" transform="translate(132.727922, 71.000000)">
|
||||
<use id="Rectangle-21" stroke="#EEEEEE" mask="url(#mask-2)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-1"></use>
|
||||
<use id="Rectangle-16-Copy" stroke="#EEEEEE" mask="url(#mask-4)" stroke-width="8" fill="#FFFFFF" transform="translate(69.866239, 93.888267) rotate(-20.000000) translate(-69.866239, -93.888267) " xlink:href="#path-3"></use>
|
||||
<use id="Rectangle-16-Copy-2" stroke="#EEEEEE" mask="url(#mask-6)" stroke-width="8" fill="#FFFFFF" transform="translate(23.155047, 93.888267) scale(-1, 1) rotate(-20.000000) translate(-23.155047, -93.888267) " xlink:href="#path-5"></use>
|
||||
<use id="Rectangle-15" stroke="#EEEEEE" mask="url(#mask-8)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-7"></use>
|
||||
<use id="Rectangle-16" stroke="#EEEEEE" mask="url(#mask-10)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-9"></use>
|
||||
<g id="Group" transform="translate(45.500000, 33.000000) rotate(20.000000) translate(-45.500000, -33.000000) translate(5.000000, 13.000000)">
|
||||
<use id="Rectangle-4" stroke="#EEEEEE" mask="url(#mask-12)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-11"></use>
|
||||
<use id="Rectangle-20" stroke="#EEEEEE" mask="url(#mask-14)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-13"></use>
|
||||
<use id="Rectangle-2" stroke="#EEEEEE" mask="url(#mask-16)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-15"></use>
|
||||
<use id="Rectangle" stroke="#EEEEEE" mask="url(#mask-18)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-17"></use>
|
||||
<rect id="Rectangle-17" fill="#EEEEEE" x="21" y="7" width="3" height="22"></rect>
|
||||
<rect id="Rectangle-17-Copy" fill="#EEEEEE" x="64" y="8" width="3" height="17"></rect>
|
||||
<circle id="Oval-9" fill="#B5A7DD" cx="40" cy="18" r="2"></circle>
|
||||
<circle id="Oval-9-Copy-4" fill="#EEEEEE" cx="47" cy="33" r="2"></circle>
|
||||
<use id="Rectangle-19" stroke="#EEEEEE" mask="url(#mask-20)" stroke-width="8" fill="#FFFFFF" xlink:href="#path-19"></use>
|
||||
</g>
|
||||
</g>
|
||||
<path d="M265.128496,225.286991 C247.289192,194.617726 214.068171,174 176.031622,174 C137.847583,174 104.51649,194.77793 86.7279221,225.644211" id="Oval-10" stroke="#EEEEEE" stroke-width="4" stroke-linecap="round" fill="#FFFFFF"></path>
|
||||
<circle id="Oval-11" stroke="#FDE5D8" stroke-width="4" fill="#FFFFFF" cx="24.5" cy="25.5" r="24.5"></circle>
|
||||
<path d="M24,1.00292933 C24,0.449026756 24.4438648,0 25,0 C25.5522847,0 26,0.437881351 26,1.00292933 L26,5.99707067 C26,6.55097324 25.5561352,7 25,7 C24.4477153,7 24,6.56211865 24,5.99707067 L24,1.00292933 Z M48.46461,17.3244238 C48.9914026,17.1532585 49.5556142,17.4366422 49.7274694,17.9655581 C49.8981348,18.4908122 49.6200365,19.0519274 49.0826439,19.2265369 L44.3329333,20.7698114 C43.8061406,20.9409767 43.241929,20.6575931 43.0700738,20.1286771 C42.8994084,19.6034231 43.1775067,19.0423078 43.7148993,18.8676984 L48.46461,17.3244238 Z M40.5019265,45.6352697 C40.8275022,46.0833863 40.7323394,46.7075538 40.2824166,47.0344419 C39.8356088,47.3590667 39.2160194,47.2679737 38.8838925,46.8108402 L35.9484099,42.770495 C35.6228341,42.3223784 35.717997,41.6982109 36.1679198,41.3713229 C36.6147275,41.0466981 37.234317,41.1377911 37.5664439,41.5949245 L40.5019265,45.6352697 Z M11.1161075,46.8108402 C10.7905317,47.2589568 10.1675063,47.3613299 9.71758344,47.0344419 C9.27077569,46.709817 9.16594665,46.0924031 9.49807352,45.6352697 L12.4335561,41.5949245 C12.7591319,41.1468079 13.3821574,41.0444348 13.8320802,41.3713229 C14.278888,41.6959477 14.383717,42.3133616 14.0515901,42.770495 L11.1161075,46.8108402 Z M0.917356057,19.2265369 C0.390563404,19.0553716 0.100675355,18.4944741 0.272530576,17.9655581 C0.44319595,17.4403041 0.997997482,17.1498144 1.53539005,17.3244238 L6.28510071,18.8676984 C6.81189336,19.0388637 7.10178141,19.5997611 6.92992619,20.1286771 C6.75926082,20.6539311 6.20445928,20.9444208 5.66706672,20.7698114 L0.917356057,19.2265369 Z" id="Rectangle-23" fill="#FDE5D8"></path>
|
||||
<rect id="Rectangle-18" fill="#FC6D26" x="24" y="14" width="3" height="12" rx="1.5"></rect>
|
||||
<rect id="Rectangle-22" fill="#FC6D26" x="24" y="24" width="12" height="3" rx="1.5"></rect>
|
||||
<circle id="Oval-11" fill="#6B4FBB" cx="25.5" cy="25.5" r="2.5"></circle>
|
||||
<path d="M358.949747,6.87474747 L357.453009,7.20729654 C356.9128,7.32732164 356.570654,6.9935311 356.692198,6.44648557 L357.024747,4.94974747 L356.692198,3.45300937 C356.572173,2.91279997 356.905964,2.57065443 357.453009,2.69219839 L358.949747,3.02474747 L360.446486,2.69219839 C360.986695,2.5721733 361.328841,2.90596384 361.207297,3.45300937 L360.874747,4.94974747 L361.207297,6.44648557 C361.327322,6.98669496 360.993531,7.32884051 360.446486,7.20729654 L358.949747,6.87474747 Z" id="Star-Copy-5" fill="#6B4FBB" transform="translate(358.949747, 4.949747) rotate(-315.000000) translate(-358.949747, -4.949747) "></path>
|
||||
<path d="M113.949747,32.8747475 L112.453009,33.2072965 C111.9128,33.3273216 111.570654,32.9935311 111.692198,32.4464856 L112.024747,30.9497475 L111.692198,29.4530094 C111.572173,28.9128 111.905964,28.5706544 112.453009,28.6921984 L113.949747,29.0247475 L115.446486,28.6921984 C115.986695,28.5721733 116.328841,28.9059638 116.207297,29.4530094 L115.874747,30.9497475 L116.207297,32.4464856 C116.327322,32.986695 115.993531,33.3288405 115.446486,33.2072965 L113.949747,32.8747475 Z" id="Star-Copy-7" fill="#B5A7DD" transform="translate(113.949747, 30.949747) rotate(-315.000000) translate(-113.949747, -30.949747) "></path>
|
||||
<path d="M329.949747,211.874747 L328.453009,212.207297 C327.9128,212.327322 327.570654,211.993531 327.692198,211.446486 L328.024747,209.949747 L327.692198,208.453009 C327.572173,207.9128 327.905964,207.570654 328.453009,207.692198 L329.949747,208.024747 L331.446486,207.692198 C331.986695,207.572173 332.328841,207.905964 332.207297,208.453009 L331.874747,209.949747 L332.207297,211.446486 C332.327322,211.986695 331.993531,212.328841 331.446486,212.207297 L329.949747,211.874747 Z" id="Star-Copy-6" fill="#B5A7DD" opacity="0.5" transform="translate(329.949747, 209.949747) rotate(-315.000000) translate(-329.949747, -209.949747) "></path>
|
||||
<path d="M265.363961,54.838961 L263.153969,55.3299826 C262.617155,55.4492534 262.280283,55.1035008 262.397939,54.5739526 L262.888961,52.363961 L262.397939,50.1539694 C262.278669,49.6171548 262.624421,49.2802831 263.153969,49.3979395 L265.363961,49.888961 L267.573953,49.3979395 C268.110767,49.2786686 268.447639,49.6244213 268.329983,50.1539694 L267.838961,52.363961 L268.329983,54.5739526 C268.449253,55.1107673 268.103501,55.4476389 267.573953,55.3299826 L265.363961,54.838961 Z" id="Star-Copy-9" fill="#FC6D26" transform="translate(265.363961, 52.363961) rotate(-315.000000) translate(-265.363961, -52.363961) "></path>
|
||||
<path d="M56.363961,142.838961 L54.1539694,143.329983 C53.6171548,143.449253 53.2802831,143.103501 53.3979395,142.573953 L53.888961,140.363961 L53.3979395,138.153969 C53.2786686,137.617155 53.6244213,137.280283 54.1539694,137.397939 L56.363961,137.888961 L58.5739526,137.397939 C59.1107673,137.278669 59.4476389,137.624421 59.3299826,138.153969 L58.838961,140.363961 L59.3299826,142.573953 C59.4492534,143.110767 59.1035008,143.447639 58.5739526,143.329983 L56.363961,142.838961 Z" id="Star-Copy-8" fill="#6B4FBB" transform="translate(56.363961, 140.363961) rotate(-315.000000) translate(-56.363961, -140.363961) "></path>
|
||||
<g id="Group-6" transform="translate(311.872633, 125.094458) rotate(-345.000000) translate(-311.872633, -125.094458) translate(290.872633, 115.094458)">
|
||||
<circle id="Oval-12" stroke="#FDE5D8" stroke-width="4" fill="#FFFFFF" cx="21" cy="10" r="10"></circle>
|
||||
<ellipse id="Oval-13" fill="#FDE5D8" cx="21" cy="10" rx="21" ry="2"></ellipse>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
After Width: | Height: | Size: 11 KiB |
|
@ -0,0 +1,25 @@
|
|||
<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>
|
After Width: | Height: | Size: 1.9 KiB |
|
@ -0,0 +1,27 @@
|
|||
<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>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -3,32 +3,38 @@
|
|||
.context.prepend-top-default
|
||||
.milestone-summary
|
||||
%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
|
||||
%span.remaining-days= milestone_remaining_days(milestone)
|
||||
%span.pull-right.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 btn-grouped", title: "New Issue" do
|
||||
New Issue
|
||||
= link_to 'Browse Issues', milestones_browse_issuables_path(milestone, type: :issues), class: "btn btn-grouped"
|
||||
%span.pull-right.tab-merge-requests-buttons.hidden
|
||||
= link_to 'Browse Merge Requests', milestones_browse_issuables_path(milestone, type: :merge_requests), class: "btn btn-grouped"
|
||||
.milestone-stats-and-buttons
|
||||
.milestone-stats
|
||||
%span.milestone-stat.with-drilldown
|
||||
%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
|
||||
%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
|
|||
include Sidekiq::Worker
|
||||
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)
|
||||
NotificationService.new.new_note(note)
|
||||
Notes::PostProcessService.new(note).execute
|
||||
|
|
|
@ -1,54 +1,38 @@
|
|||
# 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
|
||||
include Sidekiq::Worker
|
||||
include DedicatedSidekiqQueue
|
||||
|
||||
LEASE_TIMEOUT = 15.minutes.to_i
|
||||
|
||||
def self.lease_for(project_id)
|
||||
Gitlab::ExclusiveLease.
|
||||
new("project_cache_worker:#{project_id}", timeout: LEASE_TIMEOUT)
|
||||
end
|
||||
# project_id - The ID of the project for which to flush the cache.
|
||||
# refresh - An Array containing extra types of data to refresh such as
|
||||
# `:readme` to flush the README and `:changelog` to flush the
|
||||
# CHANGELOG.
|
||||
def perform(project_id, refresh = [])
|
||||
project = Project.find_by(id: project_id)
|
||||
|
||||
# Overwrite Sidekiq's implementation so we only schedule when actually needed.
|
||||
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
|
||||
return unless project && project.repository.exists?
|
||||
|
||||
def perform(project_id)
|
||||
if try_obtain_lease_for(project_id)
|
||||
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)
|
||||
end
|
||||
|
||||
def update_caches(project_id)
|
||||
project = Project.find(project_id)
|
||||
|
||||
return unless project.repository.exists?
|
||||
|
||||
project.update_repository_size
|
||||
update_repository_size(project)
|
||||
project.update_commit_count
|
||||
|
||||
if project.repository.root_ref
|
||||
project.repository.build_cache
|
||||
end
|
||||
project.repository.refresh_method_caches(refresh.map(&:to_sym))
|
||||
end
|
||||
|
||||
def try_obtain_lease_for(project_id)
|
||||
self.class.lease_for(project_id).try_obtain
|
||||
def update_repository_size(project)
|
||||
return unless try_obtain_lease_for(project.id, :update_repository_size)
|
||||
|
||||
Rails.logger.info("Updating repository size for project #{project.id}")
|
||||
|
||||
project.update_repository_size
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def try_obtain_lease_for(project_id, section)
|
||||
Gitlab::ExclusiveLease.
|
||||
new("project_cache_worker:#{project_id}:#{section}", timeout: LEASE_TIMEOUT).
|
||||
try_obtain
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Changed restricted visibility admin buttons to checkboxes
|
||||
merge_request: 7463
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Show events per stage on Cycle Analytics page
|
||||
merge_request: 23449
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Fix activity page endless scroll on large viewports
|
||||
merge_request: 7608
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Add deployment command to ChatOps
|
||||
merge_request: 7619
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Fix 500 error when group name ends with git
|
||||
merge_request: 7630
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Send credentials (currently for registry only) with build data to GitLab Runner
|
||||
merge_request: 7474
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Fix undefined error in CI linter
|
||||
merge_request: 7650
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Added permissions per stage to cycle analytics endpoint
|
||||
merge_request:
|
||||
author:
|
|
@ -1,4 +0,0 @@
|
|||
---
|
||||
title: Do not create a MergeRequestDiff record when source branch is deleted
|
||||
merge_request: 7481
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Fix errors happening when source branch of merge request is removed and then restored
|
||||
merge_request: 7568
|
||||
author:
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue