Commit 6264b2df authored by Lin Jen-Shin's avatar Lin Jen-Shin

Merge remote-tracking branch 'upstream/master' into show-commit-status-from-latest-pipeline

* upstream/master: (252 commits)
  Log mv_namespace parameters
  Remove header ids from University docs
  Added test that checks the correct select box is there for the LFS enabled setting.
  Simplify copy on "Create a new list" dropdown in Issue Boards
  Fix `LFS enabled` select box.
  Use Commit#author so we share logic and cache
  Move admin abuse report spinach test to rspec
  fixes non-retina shadow and browser zoom issue
  Use default `closest` if available!
  Adds polyfill for CustomEvent
  Move abuse report spinach test to rspec
  Add support of Chrome/Chromium in requirements.md
  Fixed dragging issues on issue boards
  Grapify the sidekiq metrics API
  Add nested groups support to the routing
  Correctly determine mergeability of MR with no discussions
  API: Add endpoint to delete a group share
  Add a starting date to milestones
  Update ProjectTeam#fetch_members to use project authorizations
  Update ProjectTeam#max_member_access_for_user_ids to use project authorizations
  ...
parents 5ea26289 3943e632
{
"env": {
"browser": true,
"es6": true
},
"extends": "airbnb",
"globals": {
"$": false,
"_": false,
"gl": false,
"gon": false,
"jQuery": false
},
"plugins": [
"filenames"
],
"rules": {
"filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"]
},
"globals": {
"$": false,
"_": false,
"beforeEach": false,
"d3": false,
"define": false,
"describe": false,
"document": false,
"expect": false,
"fixture": false,
"gl": false,
"it": false,
"jQuery": false,
"Mousetrap": false,
"spyOn": false,
"spyOnEvent": false,
"Turbolinks": false,
"window": false
}
}
......@@ -5,6 +5,7 @@
.chef
.directory
/.envrc
eslint-report.html
/.gitlab_shell_secret
.idea
/.rbenv-version
......
......@@ -20,7 +20,7 @@ before_script:
- source ./scripts/prepare_build.sh
- cp config/gitlab.yml.example config/gitlab.yml
- bundle --version
- '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"'
- '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) $FLAGS'
- retry gem install knapsack
- '[ "$SETUP_DB" != "true" ] || bundle exec rake db:drop db:create db:schema:load db:migrate add_limits_mysql'
......@@ -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
......@@ -345,13 +349,33 @@ coverage:
- coverage/index.html
- coverage/assets/
lint-javascript:
lint:javascript:
cache:
paths:
- node_modules/
stage: test
image: "node:latest"
image: "node:7.1"
before_script:
- npm install
script:
- npm --silent run eslint
lint:javascript:report:
cache:
paths:
- node_modules/
stage: post-test
image: "node:7.1"
before_script:
- npm install
script:
- npm run eslint
- find app/ spec/ -name '*.js' -or -name '*.js.es6' -exec sed --in-place 's|/\* eslint-disable .*\*/||' {} \; # run report over all files
- npm --silent run eslint-report || true # ignore exit code
artifacts:
name: eslint-report
expire_in: 31d
paths:
- eslint-report.html
# Trigger docs build
# https://gitlab.com/gitlab-com/doc-gitlab-com/blob/master/README.md#deployment-process
......@@ -377,7 +401,7 @@ notify:slack:
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "false"
script:
- ./scripts/notify_slack.sh "#builds" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/builds>"
- ./scripts/notify_slack.sh "#development" "Build on \`$CI_BUILD_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_BUILD_REF"/builds>"
when: on_failure
only:
- master@gitlab-org/gitlab-ce
......@@ -391,11 +415,13 @@ pages:
dependencies:
- coverage
- teaspoon
- lint:javascript:report
script:
- mv public/ .public/
- mkdir public/
- mv coverage public/coverage-ruby
- mv coverage-javascript/default/ public/coverage-javascript/
- mv eslint-report.html public/
artifacts:
paths:
- public
......
......@@ -4,6 +4,131 @@ entry.
## 8.14.0 (2016-11-22)
- Use separate email-token for incoming email and revert back the inactive feature. !5914
- API: allow recursive tree request. !6088 (Rebeca Mendez)
- Replace jQuery.timeago with timeago.js. !6274 (ClemMakesApps)
- Add CI notifications. Who triggered a pipeline would receive an email after the pipeline is succeeded or failed. Users could also update notification settings accordingly. !6342
- Add button to delete all merged branches. !6449 (Toon Claes)
- Finer-grained Git gargage collection. !6588
- Introduce better credential and error checking to `rake gitlab:ldap:check`. !6601
- Centralize LDAP config/filter logic. !6606
- Make system notes less intrusive. !6755
- Process commits using a dedicated Sidekiq worker. !6802
- Show random messages when the To Do list is empty. !6818 (Josep Llaneras)
- Precalculate user's authorized projects in database. !6839
- Fix record not found error on NewNoteWorker processing. !6863 (Oswaldo Ferreira)
- Show avatars in mention dropdown. !6865
- Fix expanding a collapsed diff when converting a symlink to a regular file. !6953
- Defer saving project services to the database if there are no user changes. !6958
- Omniauth auto link LDAP user falls back to find by DN when user cannot be found by UID. !7002
- Display "folders" for environments. !7015
- Make it possible to trigger builds from webhooks. !7022 (Dmitry Poray)
- Fix showing pipeline status for a given commit from correct branch. !7034
- Add link to build pipeline within individual build pages. !7082
- Add api endpoint `/groups/owned`. !7103 (Borja Aparicio)
- Add query param to filter users by external & blocked type. !7109 (Yatish Mehta)
- Issues atom feed url reflect filters on dashboard. !7114 (Lucas Deschamps)
- Add setting to only allow merge requests to be merged when all discussions are resolved. !7125 (Rodolfo Arruda)
- Remove an extra leading space from diff paste data. !7133 (Hiroyuki Sato)
- Fix trace patching feature - update the updated_at value. !7146
- Fix 404 on network page when entering non-existent git revision. !7172 (Hiroyuki Sato)
- Rewrite git blame spinach feature tests to rspec feature tests. !7197 (Lisanne Fellinger)
- Add api endpoint for creating a pipeline. !7209 (Ido Leibovich)
- Allow users to subscribe to group labels. !7215
- Reduce API calls needed when importing issues and pull requests from GitHub. !7241 (Andrew Smith (EspadaV8))
- Only skip group when it's actually a group in the "Share with group" select. !7262
- Introduce round-robin project creation to spread load over multiple shards. !7266
- Ensure merge request's "remove branch" accessors return booleans. !7267
- Fix no "Register" tab if ldap auth is enabled (#24038). !7274 (Luc Didry)
- Expose label IDs in API. !7275 (Rares Sfirlogea)
- Fix invalid filename validation on eslint. !7281
- API: Ability to retrieve version information. !7286 (Robert Schilling)
- Added ability to throttle Sidekiq Jobs. !7292
- Set default Sidekiq retries to 3. !7294
- Fix double event and ajax request call on MR page. !7298 (YarNayar)
- Unify anchor link format for MR diff files. !7298 (YarNayar)
- Require projects before creating milestone. !7301 (gfyoung)
- Fix error when using invalid branch name when creating a new pipeline. !7324
- Return 400 when creating a system hook fails. !7350 (Robert Schilling)
- Auto-close environment when branch is deleted. !7355
- Rework cache invalidation so only changed data is refreshed. !7360
- Navigation bar issuables counters reflects dashboard issuables counters. !7368 (Lucas Deschamps)
- Fix cache for commit status in commits list to respect branches. !7372
- fixes 500 error on project show when user is not logged in and project is still empty. !7376
- Removed gray button styling from todo buttons in sidebars. !7387
- Fix project records with invalid visibility_level values. !7391
- Use 'Forking in progress' title when appropriate. !7394 (Philip Karpiak)
- Fix error links in help index page. !7396 (Fu Xu)
- Add support for reply-by-email when the email only contains HTML. !7397
- [Fix] Extra divider issue in dropdown. !7398
- Project download buttons always show. !7405 (Philip Karpiak)
- Give search-input correct padding-right value. !7407 (Philip Karpiak)
- Remove additional padding on right-aligned items in MR widget. !7411 (Didem Acet)
- Fix issue causing Labels not to appear in sidebar on MR page. !7416 (Alex Sanford)
- Allow mail_room idle_timeout option to be configurable. !7423
- Fix misaligned buttons on admin builds page. !7424 (Didem Acet)
- Disable "Request Access" functionality by default for new projects and groups. !7425
- fix shibboleth misconfigurations resulting in authentication bypass. !7428
- Added Mattermost slash command. !7438
- Allow to connect Chat account with GitLab. !7450
- Make New Group form respect default visibility application setting. !7454 (Jacopo Beschi @jacopo-beschi)
- Fix Error 500 when creating a merge request that contains an image that was deleted and added. !7457
- Fix labels API by adding missing current_user parameter. !7458 (Francesco Coda Zabetta)
- Changed restricted visibility admin buttons to checkboxes. !7463
- Send credentials (currently for registry only) with build data to GitLab Runner. !7474
- Fix POST /internal/allowed to cope with gitlab-shell v4.0.0 project paths. !7480
- Adds es6-promise Polyfill. !7482
- Added colored labels to related MR list. !7486 (Didem Acet)
- Use setter for key instead AR callback. !7488 (Semyon Pupkov)
- Limit labels returned for a specific project as an administrator. !7496
- Change slack notification comment link. !7498 (Herbert Kagumba)
- Allow registering users whose username contains dots. !7500 (Timothy Andrew)
- Fix race condition during group deletion and remove stale records present due to this bug. !7528 (Timothy Andrew)
- Check all namespaces on validation of new username. !7537
- Pass correct tag target to post-receive hook when creating tag via UI. !7556
- Add help message for configuring Mattermost slash commands. !7558
- Fix typo in Build page JavaScript. !7563 (winniehell)
- Make job script a required configuration entry. !7566
- Fix errors happening when source branch of merge request is removed and then restored. !7568
- Fix a wrong "The build for this merge request failed" message. !7579
- Fix Margins look weird in Project page with pinned sidebar in project stats bar. !7580
- Fix regression causing bad error message to appear on Merge Request form. !7599 (Alex Sanford)
- Fix activity page endless scroll on large viewports. !7608
- Fix 404 on some group pages when name contains dot. !7614
- Do not create a new TODO when failed build is allowed to fail. !7618
- Add deployment command to ChatOps. !7619
- Fix 500 error when group name ends with git. !7630
- Fix undefined error in CI linter. !7650
- Show events per stage on Cycle Analytics page. !23449
- Add JIRA remotelinks and prevent duplicated closing messages.
- Fixed issue boards counter border when unauthorized.
- Add placeholder for the example text for custom hex color on label creation popup. (Luis Alonso Chavez Armendariz)
- Add an index for project_id in project_import_data to improve performance.
- Fix broken commits search.
- Assignee dropdown now searches author of issue or merge request.
- Clicking "force remove source branch" label now toggles the checkbox again.
- More aggressively preload on merge request and issue index pages.
- Fix broken link to observatory cli on Frontend Dev Guide. (Sam Rose)
- Fixing the issue of the project fork url giving 500 when not signed instead of being redirected to sign in page. (Cagdas Gerede)
- Fix: Guest sees some repository details and gets 404.
- Add logging for rack attack events to production.log.
- Add environment info to builds page.
- Allow commit note to be visible if repo is visible.
- Bump omniauth-gitlab to 1.0.2 to fix incompatibility with omniauth-oauth2.
- Redesign pipelines page.
- Faster search inside Project.
- Search for a filename in a project.
- Allow sorting groups in the API.
- Fix: Todos Filter Shows All Users.
- Use the Gitlab Workhorse HTTP header in the admin dashboard. (Chris Wright)
- Fixed multiple requests sent when opening dropdowns.
- Added permissions per stage to cycle analytics endpoint.
- Fix project Visibility Level selector not using default values.
- Add events per stage to cycle analytics.
- Allow to test JIRA service settings without having a repository.
- Fix JIRA references for project snippets.
- Allow enabling and disabling commit and MR events for JIRA.
- simplify url generation. (Jarka Kadlecova)
- Show correct environment log in admin/logs (@duk3luk3 !7191)
- Fix Milestone dropdown not stay selected for `Upcoming` and `No Milestone` option !7117
- Diff collapse won't shift when collapsing.
......@@ -32,6 +157,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
......
8.14.0-pre
8.15.0-pre
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-undef, quotes, no-var, padded-blocks, max-len */
(function() {
this.Activities = (function() {
function Activities() {
Pager.init(20, true, false, this.updateTooltips);
$(".event-filter-link").on("click", (function(_this) {
return function(event) {
event.preventDefault();
_this.toggleFilter($(event.currentTarget));
return _this.reloadActivities();
};
})(this));
}
Activities.prototype.updateTooltips = function() {
gl.utils.localTimeAgo($('.js-timeago', '.content_list'));
};
Activities.prototype.reloadActivities = function() {
$(".content_list").html('');
Pager.init(20, true, false, this.updateTooltips);
};
Activities.prototype.toggleFilter = function(sender) {
var filter = sender.attr("id").split("_")[0];
$('.event-filter .active').removeClass("active");
Cookies.set("event_filter", filter);
sender.closest('li').toggleClass("active");
};
return Activities;
})();
}).call(this);
/* eslint-disable no-param-reassign, class-methods-use-this */
/* global Pager */
/* global 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 = {}));
......@@ -54,6 +54,9 @@
mouseDown () {
this.showDetail = true;
},
mouseMove() {
this.showDetail = false;
},
showIssue (e) {
const targetTagName = e.target.tagName.toLowerCase();
......
......@@ -94,12 +94,10 @@
gl.issueBoards.onStart();
},
onAdd: (e) => {
// Add the element back to original list to allow Vue to handle DOM updates
e.from.appendChild(e.item);
gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue);
this.$nextTick(() => {
// Update the issues once we know the element has been moved
gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue);
e.item.remove();
});
},
});
......
......@@ -12,7 +12,7 @@
this.pageUrl = options.pageUrl;
this.buildUrl = options.buildUrl;
this.buildStatus = options.buildStatus;
this.state = options.state1;
this.state = options.logState;
this.buildStage = options.buildStage;
this.updateDropdown = bind(this.updateDropdown, this);
this.$document = $(document);
......
/* eslint-disable no-param-reassign */
/* global Vue */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageCodeComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
<div class="item-details">
<img class="avatar" :src="mergeRequest.author.avatarUrl">
<h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url">
{{ mergeRequest.title }}
</a>
</h5>
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot;
<span>
Opened
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span>
<span>
by
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span>
</div>
<div class="item-time">
<total-time :time="mergeRequest.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
/* global Vue */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageIssueComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
<div class="item-details">
<img class="avatar" :src="issue.author.avatarUrl">
<h5 class="item-title issue-title">
<a class="issue-title" :href="issue.url">
{{ issue.title }}
</a>
</h5>
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
&middot;
<span>
Opened
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span>
<span>
by
<a :href="issue.author.webUrl" class="issue-author-link">
{{ issue.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="issue.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
/* global Vue */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StagePlanComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="commit in items" class="stage-event-item">
<div class="item-details item-conmmit-component">
<img class="avatar" :src="commit.author.avatarUrl">
<h5 class="item-title commit-title">
<a :href="commit.commitUrl">
{{ commit.title }}
</a>
</h5>
<span>
First
<span class="commit-icon">${global.cycleAnalytics.svgs.iconCommit}</span>
<a :href="commit.commitUrl" class="commit-hash-link monospace">{{ commit.shortSha }}</a>
pushed by
<a :href="commit.author.webUrl" class="commit-author-link">
{{ commit.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="commit.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
/* global Vue */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageProductionComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="issue in items" class="stage-event-item">
<div class="item-details">
<img class="avatar" :src="issue.author.avatarUrl">
<h5 class="item-title issue-title">
<a class="issue-title" :href="issue.url">
{{ issue.title }}
</a>
</h5>
<a :href="issue.url" class="issue-link">#{{ issue.iid }}</a>
&middot;
<span>
Opened
<a :href="issue.url" class="issue-date">{{ issue.createdAt }}</a>
</span>
<span>
by
<a :href="issue.author.webUrl" class="issue-author-link">
{{ issue.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="issue.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
/* global Vue */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageReviewComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="mergeRequest in items" class="stage-event-item">
<div class="item-details">
<img class="avatar" :src="mergeRequest.author.avatarUrl">
<h5 class="item-title merge-merquest-title">
<a :href="mergeRequest.url">
{{ mergeRequest.title }}
</a>
</h5>
<a :href="mergeRequest.url" class="issue-link">!{{ mergeRequest.iid }}</a>
&middot;
<span>
Opened
<a :href="mergeRequest.url" class="issue-date">{{ mergeRequest.createdAt }}</a>
</span>
<span>
by
<a :href="mergeRequest.author.webUrl" class="issue-author-link">{{ mergeRequest.author.name }}</a>
</span>
<template v-if="mergeRequest.state === 'closed'">
<span class="merge-request-state">
<i class="fa fa-ban"></i>
{{ mergeRequest.state.toUpperCase() }}
</span>
</template>
<template v-else>
<span class="merge-request-branch" v-if="mergeRequest.branch">
<i class= "fa fa-code-fork"></i>
<a :href="mergeRequest.branch.url">{{ mergeRequest.branch.name }}</a>
</span>
</template>
</div>
<div class="item-time">
<total-time :time="mergeRequest.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
/* global Vue */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageStagingComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component">
<div class="item-details">
<img class="avatar" :src="build.author.avatarUrl">
<h5 class="item-title">
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i>
<a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
<span class="icon-branch">${global.cycleAnalytics.svgs.iconBranch}</span>
<a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
</h5>
<span>
<a :href="build.url" class="build-date">{{ build.date }}</a>
by
<a :href="build.author.webUrl" class="issue-author-link">
{{ build.author.name }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="build.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
/* global Vue */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.StageTestComponent = Vue.extend({
props: {
items: Array,
stage: Object,
},
template: `
<div>
<div class="events-description">
{{ stage.description }}
</div>
<ul class="stage-event-list">
<li v-for="build in items" class="stage-event-item item-build-component">
<div class="item-details">
<h5 class="item-title">
<span class="icon-build-status">${global.cycleAnalytics.svgs.iconBuildStatus}</span>
<a :href="build.url" class="item-build-name">{{ build.name }}</a>
&middot;
<a :href="build.url" class="pipeline-id">#{{ build.id }}</a>
<i class="fa fa-code-fork"></i>
<a :href="build.branch.url" class="branch-name monospace">{{ build.branch.name }}</a>
<span class="icon-branch">${global.cycleAnalytics.svgs.iconBranch}</span>
<a :href="build.commitUrl" class="short-sha monospace">{{ build.shortSha }}</a>
</h5>
<span>
<a :href="build.url" class="issue-date">
{{ build.date }}
</a>
</span>
</div>
<div class="item-time">
<total-time :time="build.totalTime"></total-time>
</div>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
/* global Vue */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.TotalTimeComponent = Vue.extend({
props: {
time: Object,
},
template: `
<span class="total-time">
<template v-if="time.days">{{ time.days }} <span>{{ time.days === 1 ? 'day' : 'days' }}</span></template>
<template v-if="time.hours">{{ time.hours }} <span>hr</span></template>
<template v-if="time.mins && !time.days">{{ time.mins }} <span>mins</span></template>
<template v-if="time.seconds && Object.keys(time).length === 1 || time.seconds === 0">{{ time.seconds }} <span>s</span></template>
</span>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable */
//= require vue
((global) => {
const COOKIE_NAME = 'cycle_analytics_help_dismissed';
const store = gl.cycleAnalyticsStore = {
isLoading: true,
hasError: false,
isHelpDismissed: Cookies.get(COOKIE_NAME),
analytics: {}
};
/* global Vue */
/* global Cookies */
/* global Flash */
gl.CycleAnalytics = class CycleAnalytics {
constructor() {
const that = this;
this.vue = new Vue({
el: '#cycle-analytics',
name: 'CycleAnalytics',
created: this.fetchData(),
data: store,
methods: {
dismissLanding() {
that.dismissLanding();
}
}
});
}
fetchData(options) {
store.isLoading = true;
options = options || { startDate: 30 };
$.ajax({
url: $('#cycle-analytics').data('request-path'),
method: 'GET',
dataType: 'json',
contentType: 'application/json',
data: {
cycle_analytics: {
start_date: options.startDate
}
//= require vue
//= require_tree ./svg
//= require_tree .
$(() => {
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,
});
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');
$dropdown.find('li a').off('click').on('click', (e) => {
e.preventDefault();
const $target = $(e.currentTarget);
this.startDate = $target.data('value');
$label.text($target.text().trim());
this.fetchCycleAnalyticsData({ startDate: this.startDate });
});
},
fetchCycleAnalyticsData(options) {
const fetchOptions = options || { startDate: this.startDate };
this.isLoading = true;
cycleAnalyticsService
.fetchCycleAnalyticsData(fetchOptions)
.done((response) => {
cycleAnalyticsStore.setCycleAnalyticsData(response);
this.selectDefaultStage();
this.initDropdown();
})
.error(() => {
this.handleError();
})
.always(() => {
this.isLoading = false;
});
},
selectDefaultStage() {
const stage = this.state.stages.first();
this.selectStage(stage);
},
selectStage(stage) {
if (this.isLoadingStage) return;
if (this.currentStage === stage) return;
if (!stage.isUserAllowed) {
cycleAnalyticsStore.setActiveStage(stage);
return;
}
}).done((data) => {
this.decorateData(data);
this.initDropdown();
})
.error((data) => {
this.handleError(data);
})
.always(() => {
store.isLoading = false;
})
}
decorateData(data) {
data.summary = data.summary || [];
data.stats = data.stats || [];
data.summary.forEach((item) => {
item.value = item.value || '-';
});
data.stats.forEach((item) => {
item.value = item.value || '- - -';
});
store.analytics = data;
}
handleError(data) {
store.hasError = true;
new Flash('There was an error while fetching cycle analytics data.', 'alert');
}
dismissLanding() {
store.isHelpDismissed = true;
Cookies.set(COOKIE_NAME, true);
}
initDropdown() {
const $dropdown = $('.js-ca-dropdown');
const $label = $dropdown.find('.dropdown-label');
$dropdown.find('li a').off('click').on('click', (e) => {
e.preventDefault();
const $target = $(e.currentTarget);
const value = $target.data('value');
$label.text($target.text().trim());
this.fetchData({ startDate: value });
})
}
}
})(window.gl || (window.gl = {}));
this.isLoadingStage = true;
cycleAnalyticsStore.setStageEvents([]);
cycleAnalyticsStore.setActiveStage(stage);
cycleAnalyticsService
.fetchStageData({
stage,
startDate: this.startDate,
})
.done((response) => {
this.isEmptyStage = !response.events.length;
cycleAnalyticsStore.setStageEvents(response.events);
})
.error(() => {
this.isEmptyStage = true;
})
.always(() => {
this.isLoadingStage = false;
});
},
dismissOverviewDialog() {
this.isOverviewDialogDismissed = true;
Cookies.set(OVERVIEW_DIALOG_COOKIE, '1');
},
},
});
// Register global components
Vue.component('total-time', gl.cycleAnalytics.TotalTimeComponent);
});
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
class CycleAnalyticsService {
constructor(options) {
this.requestPath = options.requestPath;
}
fetchCycleAnalyticsData(options) {
options = options || { startDate: 30 };
return $.ajax({
url: this.requestPath,
method: 'GET',
dataType: 'json',
contentType: 'application/json',
data: {
cycle_analytics: {
start_date: options.startDate,
},
},
});
}
fetchStageData(options) {
const {
stage,
startDate,
} = options;
return $.get(`${this.requestPath}/events/${stage.title.toLowerCase()}.json`, {
cycle_analytics: {
start_date: startDate,
},
});
}
}
global.cycleAnalytics.CycleAnalyticsService = CycleAnalyticsService;
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
const EMPTY_STAGE_TEXTS = {
issue: 'The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage.',
plan: 'The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit.',
code: 'The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request.',
test: 'The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running.',
review: 'The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request.',
staging: 'The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time.',
production: 'The production stage shows the total time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle.',
};
global.cycleAnalytics.CycleAnalyticsStore = {
state: {
summary: '',
stats: '',
analytics: '',
events: [],
stages: [],
},
setCycleAnalyticsData(data) {
this.state = Object.assign(this.state, this.decorateData(data));
},
decorateData(data) {
const newData = {};
newData.stages = data.stats || [];
newData.summary = data.summary || [];
newData.summary.forEach((item) => {
item.value = item.value || '-';
});
newData.stages.forEach((item) => {
const stageName = item.title.toLowerCase();
item.active = false;
item.isUserAllowed = data.permissions[stageName];
item.emptyStageText = EMPTY_STAGE_TEXTS[stageName];
item.component = `stage-${stageName}-component`;
});
newData.analytics = data;
return newData;
},
setLoadingState(state) {
this.state.isLoading = state;
},
setErrorState(state) {
this.state.hasError = state;
},
deactivateAllStages() {
this.state.stages.forEach((stage) => {
stage.active = false;
});
},
setActiveStage(stage) {
this.deactivateAllStages();
stage.active = true;
},
setStageEvents(events) {
this.state.events = this.decorateEvents(events);
},
decorateEvents(events) {
const newEvents = events;
newEvents.forEach((item) => {
item.totalTime = item.total_time;
item.author.webUrl = item.author.web_url;
item.author.avatarUrl = item.author.avatar_url;
if (item.created_at) item.createdAt = item.created_at;
if (item.short_sha) item.shortSha = item.short_sha;
if (item.commit_url) item.commitUrl = item.commit_url;
delete item.author.web_url;
delete item.author.avatar_url;
delete item.total_time;
delete item.created_at;
delete item.short_sha;
delete item.commit_url;
});
return newEvents;
},
currentActiveStage() {
return this.state.stages.find(stage => stage.active);
},
};
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {};
global.cycleAnalytics.svgs.iconBranch = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><path fill="#8C8C8C" fill-rule="evenodd" d="M9.678 6.722C9.353 5.167 8.053 4 6.5 4S3.647 5.167 3.322 6.722h-2.6c-.397 0-.722.35-.722.778 0 .428.325.778.722.778h2.6C3.647 9.833 4.947 11 6.5 11s2.853-1.167 3.178-2.722h2.6c.397 0 .722-.35.722-.778 0-.428-.325-.778-.722-.778h-2.6zM4.694 7.5c0-1.09.795-1.944 1.806-1.944 1.01 0 1.806.855 1.806 1.944 0 1.09-.795 1.944-1.806 1.944-1.01 0-1.806-.855-1.806-1.944z"/></svg>';
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {};
global.cycleAnalytics.svgs.iconBuildStatus = '<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14"><g fill="#31AF64" fill-rule="evenodd"><path d="M12.5 7c0-3.038-2.462-5.5-5.5-5.5S1.5 3.962 1.5 7s2.462 5.5 5.5 5.5 5.5-2.462 5.5-5.5zM0 7c0-3.866 3.134-7 7-7s7 3.134 7 7-3.134 7-7 7-7-3.134-7-7z"/><path d="M6.28 7.697L5.045 6.464c-.117-.117-.305-.117-.42-.002l-.614.614c-.11.113-.11.303.007.42l1.91 1.91c.19.19.51.197.703.004l.264-.265L9.997 6.04c.108-.107.107-.293-.01-.408l-.612-.614c-.114-.113-.298-.12-.41-.01L6.28 7.7z"/></g></svg>';
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((global) => {
global.cycleAnalytics = global.cycleAnalytics || {};
global.cycleAnalytics.svgs = global.cycleAnalytics.svgs || {};
global.cycleAnalytics.svgs.iconCommit = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40"><path fill="#8F8F8F" fill-rule="evenodd" d="M28.777 18c-.91-4.008-4.494-7-8.777-7-4.283 0-7.868 2.992-8.777 7H4.01C2.9 18 2 18.895 2 20c0 1.112.9 2 2.01 2h7.213c.91 4.008 4.494 7 8.777 7 4.283 0 7.868-2.992 8.777-7h7.214C37.1 22 38 21.105 38 20c0-1.112-.9-2-2.01-2h-7.213zM20 25c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5z"/></svg>';
})(window.gl || (window.gl = {}));
......@@ -110,10 +110,10 @@
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':
......
......@@ -145,25 +145,19 @@
class DueDateSelectors {
constructor() {
this.initMilestoneDueDate();
this.initMilestoneDatePicker();
this.initIssuableSelect();
}
initMilestoneDueDate() {
const $datePicker = $('.datepicker');
initMilestoneDatePicker() {
$('.datepicker').datepicker({
dateFormat: 'yy-mm-dd'
});
if ($datePicker.length) {
const $dueDate = $('#milestone_due_date');
$datePicker.datepicker({
dateFormat: 'yy-mm-dd',
onSelect: (dateText, inst) => {
$dueDate.val(dateText);
}
}).datepicker('setDate', $.datepicker.parseDate('yy-mm-dd', $dueDate.val()));
}
$('.js-clear-due-date').on('click', (e) => {
$('.js-clear-due-date,.js-clear-start-date').on('click', (e) => {
e.preventDefault();
$.datepicker._clearDate($datePicker);
const datepicker = $(e.target).siblings('.datepicker');
$.datepicker._clearDate(datepicker);
});
}
......
/* eslint-disable no-param-reassign */
/* global Vue */
/* global EnvironmentsService */
//= require vue
//= require vue-resource
//= require_tree ../services/
//= require ./environment_item
/* globals Vue, EnvironmentsService */
/* eslint-disable no-param-reassign */
(() => { // eslint-disable-line
(() => {
window.gl = window.gl || {};
/**
......@@ -157,17 +158,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 +184,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,18 +205,17 @@
</a>
</div>
<div
class="table-holder"
<div class="table-holder"
v-if="!isLoading && state.environments.length > 0">
<table class="table ci-table environments">
<thead>
<tr>
<th>Environment</th>
<th>Last deployment</th>
<th>Build</th>
<th>Commit</th>
<th></th>
<th class="hidden-xs"></th>
<th class="environments-name">Environment</th>
<th class="environments-deploy">Last deployment</th>
<th class="environments-build">Build</th>
<th class="environments-commit">Commit</th>
<th class="environments-date"></th>
<th class="hidden-xs environments-actions"></th>
</tr>
</thead>
<tbody>
......@@ -234,7 +233,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>
......
/*= require lib/utils/timeago */
/* global Vue */
/* global timeago */
/*= require timeago */
/*= require lib/utils/text_utility */
/*= require vue_common_component/commit */
/*= require ./environment_actions */
......@@ -6,8 +9,6 @@
/*= require ./environment_stop */
/*= require ./environment_rollback */
/* globals Vue, timeago */
(() => {
/**
* Envrionment Item Component
......@@ -389,11 +390,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 +401,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 +430,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 +454,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"
......
......@@ -3,7 +3,7 @@
Element.prototype.matches = Element.prototype.matches || Element.prototype.msMatchesSelector;
Element.prototype.closest = function closest(selector, selectedElement = this) {
Element.prototype.closest = Element.prototype.closest || function closest(selector, selectedElement = this) {
if (!selectedElement) return;
return selectedElement.matches(selector) ? selectedElement : Element.prototype.closest(selector, selectedElement.parentElement);
};
/**
* CustomEvent support for IE
*/
if (typeof window.CustomEvent !== 'function') {
window.CustomEvent = function CustomEvent(e, params) {
const options = params || { bubbles: false, cancelable: false, detail: undefined };
const evt = document.createEvent('CustomEvent');
evt.initCustomEvent(e, options.bubbles, options.cancelable, options.detail);
return evt;
};
window.CustomEvent.prototype = window.Event.prototype;
}
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, no-undef, comma-dangle, no-unused-expressions, prefer-template, padded-blocks, max-len */
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, no-param-reassign, no-cond-assign, comma-dangle, no-unused-expressions, prefer-template, padded-blocks, max-len */
/* global timeago */
/* global dateFormat */
/*= require timeago */
/*= require date.format */
(function() {
(function(w) {
var base;
......
(() => {
/*
* 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 = {}));
/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, padded-blocks */
/* global Turbolinks */
(function() {
Turbolinks.enableProgressBar();
......
......@@ -67,7 +67,7 @@
MergeRequestWidget.prototype.addEventListeners = function() {
var allowedPages;
allowedPages = ['show', 'commits', 'builds', 'pipelines', 'changes'];
return $(document).on('page:change.merge_request', (function(_this) {
$(document).on('page:change.merge_request', (function(_this) {
return function() {
var page;
page = $('body').data('page').split(':').last();
......@@ -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 += '.';
......@@ -245,7 +245,7 @@
case "not_found":
return this.setMergeButtonClass('btn-danger');
case "running":
return this.setMergeButtonClass('btn-warning');
return this.setMergeButtonClass('btn-info');
case "success":
case "success_with_warnings":
return this.setMergeButtonClass('btn-create');
......@@ -263,7 +263,7 @@
};
MergeRequestWidget.prototype.setMergeButtonClass = function(css_class) {
return $('.js-merge-button,.accept-action .dropdown-toggle').removeClass('btn-danger btn-warning btn-create').addClass(css_class);
return $('.js-merge-button,.accept-action .dropdown-toggle').removeClass('btn-danger btn-info btn-create').addClass(css_class);
};
return MergeRequestWidget;
......
......@@ -113,6 +113,7 @@
$(document).off("click", ".js-note-discard");
$(document).off("keydown", ".js-note-text");
$(document).off('click', '.js-comment-resolve-button');
$(document).off("click", '.system-note-commit-list-toggler');
$('.note .js-task-list-container').taskList('disable');
return $(document).off('tasklist:changed', '.note .js-task-list-container');
};
......
/* eslint-disable func-names, space-before-function-paren, object-shorthand, quotes, no-undef, prefer-template, wrap-iife, comma-dangle, no-return-assign, no-else-return, consistent-return, no-unused-vars, padded-blocks, max-len */
(function() {
this.Pager = {
init: function(limit, preload, disable, callback) {
this.limit = limit != null ? limit : 0;
this.disable = disable != null ? disable : false;
this.callback = callback != null ? callback : $.noop;
this.loading = $('.loading').first();
if (preload) {
this.offset = 0;
this.getOld();
} else {
this.offset = this.limit;
}
return this.initLoadMore();
},
getOld: function() {
this.loading.show();
return $.ajax({
type: "GET",
url: $(".content_list").data('href') || location.href,
data: "limit=" + this.limit + "&offset=" + this.offset,
complete: (function(_this) {
return function() {
return _this.loading.hide();
};
})(this),
success: function(data) {
Pager.append(data.count, data.html);
return Pager.callback();
},
dataType: "json"
});
},
append: function(count, html) {
$(".content_list").append(html);
if (count > 0) {
return this.offset += count;
} else {
return this.disable = true;
}
},
initLoadMore: function() {
$(document).unbind('scroll');
return $(document).endlessScroll({
bottomPixels: 400,
fireDelay: 1000,
fireOnce: true,
ceaseFire: function() {
return Pager.disable;
},
callback: (function(_this) {
return function(i) {
if (!_this.loading.is(':visible')) {
_this.loading.show();
return Pager.getOld();
}
};
})(this)
});
}
};
}).call(this);
(() => {
const ENDLESS_SCROLL_BOTTOM_PX = 400;
const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000;
const Pager = {
init(limit = 0, preload = false, disable = false, callback = $.noop) {
this.limit = limit;
this.offset = this.limit;
this.disable = disable;
this.callback = callback;
this.loading = $('.loading').first();
if (preload) {
this.offset = 0;
this.getOld();
}
this.initLoadMore();
},
getOld() {
this.loading.show();
$.ajax({
type: 'GET',
url: $('.content_list').data('href') || window.location.href,
data: `limit=${this.limit}&offset=${this.offset}`,
dataType: 'json',
error: () => this.loading.hide(),
success: (data) => {
this.append(data.count, data.html);
this.callback();
// keep loading until we've filled the viewport height
if (!this.disable && !this.isScrollable()) {
this.getOld();
} else {
this.loading.hide();
}
},
});
},
append(count, html) {
$('.content_list').append(html);
if (count > 0) {
this.offset += count;
} else {
this.disable = true;
}
},
isScrollable() {
const $w = $(window);
return $(document).height() > $w.height() + $w.scrollTop() + ENDLESS_SCROLL_BOTTOM_PX;
},
initLoadMore() {
$(document).unbind('scroll');
$(document).endlessScroll({
bottomPixels: ENDLESS_SCROLL_BOTTOM_PX,
fireDelay: ENDLESS_SCROLL_FIRE_DELAY_MS,
fireOnce: true,
ceaseFire: () => this.disable === true,
callback: () => {
if (!this.loading.is(':visible')) {
this.loading.show();
this.getOld();
}
},
});
},
};
window.Pager = Pager;
})();
......@@ -35,7 +35,6 @@
}
onSubmitForm(e) {
e.preventDefault();
return this.saveForm();
}
......
/*
* Instances of SmartInterval extend the functionality of `setInterval`, make it configurable
* and controllable by a public API.
*
* */
(() => {
class SmartInterval {
/**
* @param { function } callback Function to be called on each iteration (required)
* @param { milliseconds } startingInterval `currentInterval` is set to this initially
* @param { milliseconds } maxInterval `currentInterval` will be incremented to this
* @param { integer } incrementByFactorOf `currentInterval` is incremented by this factor
* @param { boolean } lazyStart Configure if timer is initialized on instantiation or lazily
*/
constructor({ callback, startingInterval, maxInterval, incrementByFactorOf, lazyStart }) {
this.cfg = {
callback,
startingInterval,
maxInterval,
incrementByFactorOf,
lazyStart,
};
this.state = {
intervalId: null,
currentInterval: startingInterval,
pageVisibility: 'visible',
};
this.initInterval();
}
/* public */
start() {
const cfg = this.cfg;
const state = this.state;
state.intervalId = window.setInterval(() => {
cfg.callback();
if (this.getCurrentInterval() === cfg.maxInterval) {
return;
}
this.incrementInterval();
this.resume();
}, this.getCurrentInterval());
}
// cancel the existing timer, setting the currentInterval back to startingInterval
cancel() {
this.setCurrentInterval(this.cfg.startingInterval);
this.stopTimer();
}
// start a timer, using the existing interval
resume() {
this.stopTimer(); // stop exsiting timer, in case timer was not previously stopped
this.start();
}
destroy() {
this.cancel();
$(document).off('visibilitychange').off('page:before-unload');
}
/* private */
initInterval() {
const cfg = this.cfg;
if (!cfg.lazyStart) {
this.start();
}
this.initVisibilityChangeHandling();
this.initPageUnloadHandling();
}
initVisibilityChangeHandling() {
// cancel interval when tab no longer shown (prevents cached pages from polling)
$(document)
.off('visibilitychange').on('visibilitychange', (e) => {
this.state.pageVisibility = e.target.visibilityState;
this.handleVisibilityChange();
});
}
initPageUnloadHandling() {
// prevent interval continuing after page change, when kept in cache by Turbolinks
$(document).on('page:before-unload', () => this.cancel());
}
handleVisibilityChange() {
const state = this.state;
const intervalAction = state.pageVisibility === 'hidden' ? this.cancel : this.resume;
intervalAction.apply(this);
}
getCurrentInterval() {
return this.state.currentInterval;
}
setCurrentInterval(newInterval) {
this.state.currentInterval = newInterval;
}
incrementInterval() {
const cfg = this.cfg;
const currentInterval = this.getCurrentInterval();
let nextInterval = currentInterval * cfg.incrementByFactorOf;
if (nextInterval > cfg.maxInterval) {
nextInterval = cfg.maxInterval;
}
this.setCurrentInterval(nextInterval);
}
stopTimer() {
const state = this.state;
state.intervalId = window.clearInterval(state.intervalId);
}
}
gl.SmartInterval = SmartInterval;
})(window.gl || (window.gl = {}));
//= require vue
//= require vue-resource
(() => {
/*
* SubbableResource can be extended to provide a pubsub-style service for one-off REST
* calls. Subscribe by passing a callback or render method you will use to handle responses.
*
* */
class SubbableResource {
constructor(resourcePath) {
this.endpoint = resourcePath;
// TODO: Switch to axios.create
this.resource = $.ajax;
this.subscribers = [];
}
subscribe(callback) {
this.subscribers.push(callback);
}
publish(newResponse) {
const responseCopy = _.extend({}, newResponse);
this.subscribers.forEach((fn) => {
fn(responseCopy);
});
return newResponse;
}
get(payload) {
return this.resource(payload)
.then(data => this.publish(data));
}
post(payload) {
return this.resource(payload)
.then(data => this.publish(data));
}
put(payload) {
return this.resource(payload)
.then(data => this.publish(data));
}
delete(payload) {
return this.resource(payload)
.then(data => this.publish(data));
}
}
gl.SubbableResource = SubbableResource;
})(window.gl || (window.gl = {}));
/* eslint-disable func-names, space-before-function-paren, wrap-iife, max-len, quotes, consistent-return, no-var, one-var, one-var-declaration-per-line, no-else-return, prefer-arrow-callback, padded-blocks, max-len */
/* global Turbolinks */
(function() {
this.TreeView = (function() {
function TreeView() {
......
......@@ -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;
}
......
......@@ -206,6 +206,7 @@
}
});
} else {
this.currentSelectedDate = '';
return $('.user-calendar-activities').html('');
}
};
......
......@@ -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%;
}
}
}
......@@ -299,6 +299,10 @@ table {
.well {
margin-bottom: $gl-padding;
hr {
border-color: $gray-darker;
}
}
.search_box {
......
......@@ -68,6 +68,46 @@ label {
}
}
.help-form .form-group {
margin-left: 0;
margin-right: 0;
.control-label {
font-weight: bold;
padding-top: 4px;
}
.form-control {
height: 29px;
background: $white-light;
font-family: $monospace_font;
}
.input-group-btn .btn {
padding: 3px $gl-btn-padding;
background-color: $gray-light;
border: 1px solid $border-color;
}
.text-block {
line-height: 0.8;
padding-top: 9px;
code {
line-height: 1.8;
}
}
@media(max-width: $screen-sm-min) {
padding: 0 $gl-padding;
.control-label,
.text-block {
padding-left: 0;
}
}
}
.fieldset-form fieldset {
margin-bottom: 20px;
}
......@@ -167,4 +207,3 @@ label {
color: $gl-text-color;
}
}
......@@ -39,4 +39,8 @@
&.status-box-expired {
background: #cea61b;
}
&.status-box-upcoming {
background: #8f8f8f;
}
}
......@@ -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
......
......@@ -243,7 +243,7 @@
}
.issue-boards-search {
width: 335px;
width: 290px;
.form-control {
display: inline-block;
......
#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;
}
}
}
......@@ -18,6 +18,31 @@
.environments {
table-layout: fixed;
.environments-commit,
.environments-actions,
.environments-deploy,
.environments-build,
.environments-date {
position: static;
float: none;
display: table-cell;
}
.environments-commit,
.environments-actions {
width: 20%;
}
.environments-deploy,
.environments-build,
.environments-date {
width: 10%;
}
.environments-name {
width: 30%;
}
.deployment-column {
.avatar {
float: none;
......
// CI icon colors
.ci-status-icon-success {
color: $gl-success;
.ci-status-icon {
&-created {
fill: $gray-darkest;
svg {
fill: $gl-success;
}
}
.ci-status-icon-failed {
color: $gl-danger;
svg {
fill: $gl-danger;
}
}
.ci-status-icon-pending,
.ci-status-icon-success_with_warnings {
color: $gl-warning;
svg {
fill: $gl-warning;
}
}
.ci-status-icon-running {
color: $blue-normal;
svg {
fill: $blue-normal;
}
}
.ci-status-icon-canceled,
.ci-status-icon-disabled,
.ci-status-icon-not-found {
color: $gl-gray;
svg {
fill: $gl-gray;
}
}
&-skipped,
&-canceled {
fill: $gl-text-color;
.ci-status-icon-created,
.ci-status-icon-skipped {
color: $gray-darkest;
svg {
fill: $gray-darkest;
}
}
......@@ -24,7 +24,7 @@
.accept_merge_request {
&.ci-pending,
&.ci-running {
@include btn-orange;
@include btn-blue;
}
&.ci-skipped,
......@@ -62,6 +62,7 @@
.ci_widget {
border-bottom: 1px solid $well-inner-border;
color: $gl-gray;
svg {
margin-right: 4px;
......@@ -70,48 +71,12 @@
overflow: visible;
}
&.ci-success {
color: $gl-success;
a.environment,
a.pipeline {
color: inherit;
}
}
&.ci-success_with_warnings {
color: $gl-success;
i {
color: $gl-warning;
}
}
&.ci-skipped {
background-color: #eee;
color: #888;
}
&.ci-pending {
color: $gl-warning;
}
&.ci-running {
color: $blue-normal;
}
&.ci-failed,
&.ci-error {
color: $gl-danger;
}
&.ci-canceled {
color: $gl-gray;
}
a.monospace {
color: inherit;
}
}
.mr-widget-body,
......
......@@ -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;
}
}
}
......@@ -43,12 +43,25 @@ ul.notes {
}
.system-note-message {
text-transform: lowercase;
display: inline-block;
&::first-letter {
text-transform: lowercase;
}
a {
color: $gl-link-color;
text-decoration: none;
}
p {
display: inline-block;
margin: 0;
&::first-letter {
text-transform: lowercase;
}
}
}
.timeline-content {
......@@ -62,6 +75,13 @@ ul.notes {
display: none;
padding: 10px 0 0;
cursor: pointer;
position: relative;
z-index: 2;
&:hover {
color: $gl-link-color;
text-decoration: underline;
}
}
.note-text {
......@@ -87,14 +107,24 @@ ul.notes {
display: none;
}
p:last-child {
a {
color: $gl-text-color;
&:hover {
color: $gl-link-color;
}
}
}
&::after {
content: '';
width: 100%;
height: 20px;
height: 67px;
position: absolute;
left: 0;
bottom: 50px;
background: linear-gradient(rgba($gray-light, .3) 0, $white-light);
bottom: 0;
background: linear-gradient(rgba($gray-light, 0.1) -100px, $white-light 100%);
}
&.hide-shade {
......@@ -188,11 +218,6 @@ ul.notes {
padding-bottom: 3px;
padding-right: 20px;
p {
display: inline;
margin: 0;
}
@media (min-width: $screen-sm-min) {
padding-right: 0;
}
......
......@@ -91,14 +91,6 @@
}
}
.ci-status {
svg {
top: 1px;
margin-right: 0;
}
}
a:hover {
text-decoration: none;
}
......@@ -195,7 +187,7 @@
width: 8px;
position: absolute;
right: -7px;
bottom: 8px;
bottom: 9px;
border-bottom: 2px solid $border-color;
}
}
......@@ -691,10 +683,3 @@
}
}
}
.ci-status-icon-created {
svg {
fill: $gray-darkest;
}
}
......@@ -20,3 +20,7 @@
.danger-title {
color: $gl-danger;
}
.service-settings .control-label {
padding-top: 0;
}
......@@ -11,80 +11,108 @@
text-decoration: none;
}
svg {
height: 13px;
width: 13px;
position: relative;
top: 1px;
margin-right: 3px;
overflow: visible;
}
&.ci-failed {
color: $gl-danger;
border-color: $gl-danger;
&:not(span):hover {
background-color: rgba( $gl-danger, .07);
}
svg {
fill: $gl-danger;
}
}
&.ci-success,
&.ci-success_with_warnings {
color: $gl-success;
border-color: $gl-success;
&:not(span):hover {
background-color: rgba( $gl-success, .07);
}
svg {
fill: $gl-success;
}
}
&.ci-info {
color: $gl-info;
border-color: $gl-info;
&:not(span):hover {
background-color: rgba( $gl-info, .07);
}
svg {
fill: $gl-info;
}
}
&.ci-canceled,
&.ci-skipped,
&.ci-disabled {
color: $gl-gray;
border-color: $gl-gray;
&:not(span):hover {
background-color: rgba( $gl-gray, .07);
}
svg {
fill: $gl-gray;
}
}
&.ci-pending {
color: $gl-warning;
border-color: $gl-warning;
&:not(span):hover {
background-color: rgba( $gl-warning, .07);
}
svg {
fill: $gl-warning;
}
}
&.ci-running {
color: $blue-normal;
border-color: $blue-normal;
&:not(span):hover {
background-color: rgba( $blue-normal, .07);
}
svg {
fill: $blue-normal;
}
}
&.ci-created {
&.ci-created,
&.ci-skipped {
color: $table-text-gray;
border-color: $table-text-gray;
&:not(span):hover {
background-color: rgba( $table-text-gray, .07);
}
svg {
fill: $table-text-gray;
}
}
svg {
height: 13px;
width: 13px;
position: relative;
top: 1px;
margin: 0 3px;
overflow: visible;
}
}
.ci-status-icon-success {
color: $gl-success;
}
.ci-status-icon-failed {
color: $gl-danger;
}
.ci-status-icon-pending,
.ci-status-icon-success_with_warning {
color: $gl-warning;
}
.ci-status-icon-running {
color: $blue-normal;
}
.ci-status-icon-canceled,
.ci-status-icon-disabled,
.ci-status-icon-not-found,
.ci-status-icon-skipped {
color: $gl-gray;
}
}
......
......@@ -49,6 +49,14 @@ class ApplicationController < ActionController::Base
render_404
end
def route_not_found
if current_user
not_found
else
redirect_to new_user_session_path
end
end
protected
# This filter handles both private tokens and personal access tokens
......@@ -224,7 +232,7 @@ class ApplicationController < ActionController::Base
end
def require_email
if current_user && current_user.temp_oauth_email?
if current_user && current_user.temp_oauth_email? && session[:impersonator_id].nil?
redirect_to profile_path, notice: 'Please complete your profile with email address' and return
end
end
......
......@@ -67,7 +67,7 @@ class Groups::MilestonesController < Groups::ApplicationController
end
def milestone_params
params.require(:milestone).permit(:title, :description, :due_date, :state_event)
params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event)
end
def milestone_path(title)
......
......@@ -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 = []
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_description)|
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
}
......@@ -563,11 +563,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def define_pipelines_vars
@pipelines = @merge_request.all_pipelines
if @pipelines.present?
@pipeline = @pipelines.first
@statuses = @pipeline.statuses.relevant
end
@pipeline = @merge_request.pipeline
@statuses = @pipeline.statuses.relevant if @pipeline.present?
end
def define_new_vars
......
......@@ -112,6 +112,6 @@ class Projects::MilestonesController < Projects::ApplicationController
end
def milestone_params
params.require(:milestone).permit(:title, :description, :due_date, :state_event)
params.require(:milestone).permit(:title, :description, :start_date, :due_date, :state_event)
end
end
......@@ -17,7 +17,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
flash[:notice] = "CI/CD Pipelines settings for '#{@project.name}' were successfully updated."
redirect_to namespace_project_pipelines_settings_path(@project.namespace, @project)
else
render 'index'
render 'show'
end
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
......@@ -67,14 +67,14 @@ module ApplicationSettingsHelper
def import_sources_checkboxes(help_block_id)
Gitlab::ImportSources.options.map do |name, source|
checked = current_application_settings.import_sources.include?(source)
css_class = 'btn'
css_class += ' active' if checked
css_class = checked ? 'active' : ''
checkbox_name = 'application_setting[import_sources][]'
label_tag(checkbox_name, class: css_class) do
label_tag(name, class: css_class) do
check_box_tag(checkbox_name, source, checked,
autocomplete: 'off',
'aria-describedby' => help_block_id) + name
'aria-describedby' => help_block_id,
id: name.tr(' ', '_')) + name
end
end
end
......
......@@ -12,7 +12,7 @@ module BuildsHelper
build_url: namespace_project_build_url(@project.namespace, @project, @build, :json),
build_status: @build.status,
build_stage: @build.stage,
state1: @build.trace_with_state[:state]
log_state: @build.trace_with_state[:state].to_s
}
end
end
......@@ -5,7 +5,7 @@ module CiStatusHelper
end
def ci_status_with_icon(status, target = nil)
content = ci_icon_for_status(status) + '&nbsp;'.html_safe + ci_label_for_status(status)
content = ci_icon_for_status(status) + ci_label_for_status(status)
klass = "ci-status ci-#{status}"
if target
link_to content, target, class: klass
......
......@@ -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
......@@ -64,6 +64,8 @@ module IssuesHelper
'status-box-merged'
elsif item.closed?
'status-box-closed'
elsif item.respond_to?(:upcoming?) && item.upcoming?
'status-box-upcoming'
else
'status-box-open'
end
......
......@@ -86,6 +86,30 @@ module MilestonesHelper
days = milestone.remaining_days
content = content_tag(:strong, days)
content << " #{'day'.pluralize(days)} remaining"
elsif milestone.upcoming?
content_tag(:strong, 'Upcoming')
elsif milestone.start_date && milestone.start_date.past?
days = milestone.elapsed_days
content = content_tag(:strong, days)
content << " #{'day'.pluralize(days)} elapsed"
end
end
def milestone_date_range(milestone)
if milestone.start_date && milestone.due_date
"#{milestone.start_date.to_s(:medium)} - #{milestone.due_date.to_s(:medium)}"
elsif milestone.due_date
if milestone.due_date.past?
"expired on #{milestone.due_date.to_s(:medium)}"
else
"expires on #{milestone.due_date.to_s(:medium)}"
end
elsif milestone.start_date
if milestone.start_date.past?
"started on #{milestone.start_date.to_s(:medium)}"
else
"starts on #{milestone.start_date.to_s(:medium)}"
end
end
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
......@@ -5,15 +5,11 @@ module SidekiqHelper
(?<mem>[\d\.,]+)\s+
(?<state>[DRSTWXZNLsl\+<]+)\s+
(?<start>.+)\s+
(?<command>sidekiq.*\])\s*
(?<command>sidekiq.*\])
\z/x
def parse_sidekiq_ps(line)
match = line.match(SIDEKIQ_PS_REGEXP)
if match
match[1..6]
else
%w[? ? ? ? ? ?]
end
match = line.strip.match(SIDEKIQ_PS_REGEXP)
match ? match[1..6] : Array.new(6, '?')
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
......
......@@ -135,15 +135,19 @@ class CommitStatus < ActiveRecord::Base
allow_failure? && (failed? || canceled?)
end
def duration
calculate_duration
end
def playable?
false
end
def duration
calculate_duration
def stuck?
false
end
def stuck?
def has_trace?
false
end
end
# == 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.
......
......@@ -23,7 +23,31 @@ module Milestoneish
(due_date - Date.today).to_i
end
def elapsed_days
return 0 if !start_date || start_date.future?
(Date.today - start_date).to_i
end
def issues_visible_to_user(user = nil)
issues.visible_to_user(user)
end
def upcoming?
start_date && start_date.future?
end
def expires_at
if due_date
if due_date.past?
"expired on #{due_date.to_s(:medium)}"
else
"expires on #{due_date.to_s(:medium)}"
end
end
end
def expired?
due_date && due_date.past?
end
end
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
......@@ -28,26 +28,16 @@ class GlobalMilestone
@title.to_slug.normalize.to_s
end
def expired?
if due_date
due_date.past?
else
false
end
end
def projects
@projects ||= Project.for_milestones(milestones.select(:id))
end
def state
state = milestones.map { |milestone| milestone.state }
if state.count('closed') == state.size
'closed'
else
'active'
milestones.each do |milestone|
return 'active' if milestone.state != 'closed'
end
'closed'
end
def active?
......@@ -81,18 +71,15 @@ class GlobalMilestone
@due_date =
if @milestones.all? { |x| x.due_date == @milestones.first.due_date }
@milestones.first.due_date
else
nil
end
end
def expires_at
if due_date
if due_date.past?
"expired on #{due_date.to_s(:medium)}"
else
"expires on #{due_date.to_s(:medium)}"
def start_date
return @start_date if defined?(@start_date)
@start_date =
if @milestones.all? { |x| x.start_date == @milestones.first.start_date }
@milestones.first.start_date
end
end
end
end
......@@ -65,7 +65,9 @@ class Group < Namespace
def select_for_project_authorization
if current_scope.joins_values.include?(:shared_projects)
select("members.user_id, projects.id AS project_id, project_group_links.group_access")
joins('INNER JOIN namespaces project_namespace ON project_namespace.id = projects.namespace_id')
.where('project_namespace.share_with_group_lock = ?', false)
.select("members.user_id, projects.id AS project_id, LEAST(project_group_links.group_access, members.access_level) AS access_level")
else
super
end
......
......@@ -93,7 +93,7 @@ class Issue < ActiveRecord::Base
# Check if we are scoped to a specific project's issues
if owner_project
if owner_project.authorized_for_user?(user, Gitlab::Access::REPORTER)
if owner_project.team.member?(user, Gitlab::Access::REPORTER)
# If the project is authorized for the user, they can see all issues in the project
return all
else
......
......@@ -246,7 +246,7 @@ class Member < ActiveRecord::Base
end
def post_update_hook
# override in subclass
UserProjectAccessChangedService.new(user.id).execute if access_level_changed?
end
def post_destroy_hook
......
......@@ -494,10 +494,14 @@ class MergeRequest < ActiveRecord::Base
discussions_resolvable? && diff_discussions.none?(&:to_be_resolved?)
end
def discussions_to_be_resolved?
discussions_resolvable? && !discussions_resolved?
end
def mergeable_discussions_state?
return true unless project.only_allow_merge_if_all_discussions_are_resolved?
discussions_resolved?
!discussions_to_be_resolved?
end
def hook_attrs
......@@ -686,7 +690,7 @@ class MergeRequest < ActiveRecord::Base
def mergeable_ci_state?
return true unless project.only_allow_merge_if_build_succeeds?
!pipeline || pipeline.success?
!pipeline || pipeline.success? || pipeline.skipped?
end
def environments
......
......@@ -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.
......
......@@ -29,6 +29,7 @@ class Milestone < ActiveRecord::Base
validates :title, presence: true, uniqueness: { scope: :project_id }
validates :project, presence: true
validate :start_date_should_be_less_than_due_date, if: Proc.new { |m| m.start_date.present? && m.due_date.present? }
strip_attributes :title
......@@ -131,24 +132,6 @@ class Milestone < ActiveRecord::Base
self.title
end
def expired?
if due_date
due_date.past?
else
false
end
end
def expires_at
if due_date
if due_date.past?
"expired on #{due_date.to_s(:medium)}"
else
"expires on #{due_date.to_s(:medium)}"
end
end
end
def can_be_closed?
active? && issues.opened.count.zero?
end
......@@ -212,4 +195,10 @@ class Milestone < ActiveRecord::Base
def sanitize_title(value)
CGI.unescape_html(Sanitize.clean(value.to_s))
end
def start_date_should_be_less_than_due_date
if due_date <= start_date
errors.add(:start_date, "Can't be greater than due date")
end
end
end
......@@ -27,6 +27,7 @@ class Namespace < ActiveRecord::Base
delegate :name, to: :owner, allow_nil: true, prefix: true
after_update :move_dir, if: :path_changed?
after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') }
# Save the storage paths before the projects are destroyed to use them on after destroy
before_destroy(prepend: true) { @old_repository_storage_paths = repository_storage_paths }
......@@ -103,6 +104,8 @@ class Namespace < ActiveRecord::Base
gitlab_shell.add_namespace(repository_storage_path, path_was)
unless gitlab_shell.mv_namespace(repository_storage_path, path_was, path)
Rails.logger.error "Exception moving path #{repository_storage_path} from #{path_was} to #{path}"
# if we cannot move namespace directory we should rollback
# db changes in order to prevent out of sync between db and fs
raise Exception.new('namespace directory cannot be moved')
......@@ -175,4 +178,11 @@ class Namespace < ActiveRecord::Base
end
end
end
def refresh_access_of_projects_invited_groups
Group.
joins(project_group_links: :project).
where(projects: { namespace_id: id }).
find_each(&:refresh_members_authorized_projects)
end
end
......@@ -126,6 +126,8 @@ class Project < ActiveRecord::Base
has_many :hooks, dependent: :destroy, class_name: 'ProjectHook'
has_many :protected_branches, dependent: :destroy
has_many :project_authorizations, dependent: :destroy
has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source
alias_method :members, :project_members
has_many :users, through: :project_members
......@@ -163,6 +165,7 @@ class Project < ActiveRecord::Base
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
delegate :add_user, to: :team
delegate :add_guest, :add_reporter, :add_developer, :add_master, to: :team
# Validations
validates :creator, presence: true, on: :create
......@@ -174,6 +177,7 @@ class Project < ActiveRecord::Base
message: Gitlab::Regex.project_name_regex_message }
validates :path,
presence: true,
project_path: true,
length: { within: 0..255 },
format: { with: Gitlab::Regex.project_path_regex,
message: Gitlab::Regex.project_path_regex_message }
......@@ -1086,7 +1090,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
......@@ -1292,14 +1296,6 @@ class Project < ActiveRecord::Base
end
end
# Checks if `user` is authorized for this project, with at least the
# `min_access_level` (if given).
def authorized_for_user?(user, min_access_level = nil)
return false unless user
user.authorized_project?(self, min_access_level)
end
def append_or_update_attribute(name, value)
old_values = public_send(name.to_s)
......
......@@ -57,9 +57,9 @@ class JiraService < IssueTrackerService
end
def help
'See the ' \
'[integration doc](http://doc.gitlab.com/ce/integration/external-issue-tracker.html) '\
'for details.'
'You need to configure JIRA before enabling this service. For more details
read the
[JIRA service documentation](https://docs.gitlab.com/ce/project_services/jira.html).'
end
def title
......@@ -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
......
......@@ -19,13 +19,6 @@ class MattermostSlashCommandsService < ChatService
'mattermost_slash_commands'
end
def help
"This service allows you to use slash commands with your Mattermost installation.<br/>
To setup this Service you need to create a new <b>Slash commands</b> in your Mattermost integration panel.<br/>
<br/>
Create integration with URL #{service_trigger_url(self)} and enter the token below."
end
def fields
[
{ type: 'text', name: 'token', placeholder: '' }
......
......@@ -21,6 +21,22 @@ class ProjectTeam
end
end
def add_guest(user, current_user: nil)
self << [user, :guest, current_user]
end
def add_reporter(user, current_user: nil)
self << [user, :reporter, current_user]
end
def add_developer(user, current_user: nil)
self << [user, :developer, current_user]
end
def add_master(user, current_user: nil)
self << [user, :master, current_user]
end
def find_member(user_id)
member = project.members.find_by(user_id: user_id)
......@@ -64,19 +80,19 @@ class ProjectTeam
alias_method :users, :members
def guests
@guests ||= fetch_members(:guests)
@guests ||= fetch_members(Gitlab::Access::GUEST)
end
def reporters
@reporters ||= fetch_members(:reporters)
@reporters ||= fetch_members(Gitlab::Access::REPORTER)
end
def developers
@developers ||= fetch_members(:developers)
@developers ||= fetch_members(Gitlab::Access::DEVELOPER)
end
def masters
@masters ||= fetch_members(:masters)
@masters ||= fetch_members(Gitlab::Access::MASTER)
end
def import(source_project, current_user = nil)
......@@ -125,8 +141,12 @@ class ProjectTeam
max_member_access(user.id) == Gitlab::Access::MASTER
end
def member?(user, min_member_access = Gitlab::Access::GUEST)
max_member_access(user.id) >= min_member_access
# Checks if `user` is authorized for this project, with at least the
# `min_access_level` (if given).
def member?(user, min_access_level = Gitlab::Access::GUEST)
return false unless user
user.authorized_project?(project, min_access_level)
end
def human_max_access(user_id)
......@@ -149,112 +169,29 @@ class ProjectTeam
# Lookup only the IDs we need
user_ids = user_ids - access.keys
users_access = project.project_authorizations.
where(user: user_ids).
group(:user_id).
maximum(:access_level)
if user_ids.present?
user_ids.each { |id| access[id] = Gitlab::Access::NO_ACCESS }
member_access = project.members.access_for_user_ids(user_ids)
merge_max!(access, member_access)
if group
group_access = group.members.access_for_user_ids(user_ids)
merge_max!(access, group_access)
end
# Each group produces a list of maximum access level per user. We take the
# max of the values produced by each group.
if project_shared_with_group?
project.project_group_links.each do |group_link|
invited_access = max_invited_level_for_users(group_link, user_ids)
merge_max!(access, invited_access)
end
end
end
access.merge!(users_access)
access
end
def max_member_access(user_id)
max_member_access_for_user_ids([user_id])[user_id]
max_member_access_for_user_ids([user_id])[user_id] || Gitlab::Access::NO_ACCESS
end
private
# For a given group, return the maximum access level for the user. This is the min of
# the invited access level of the group and the access level of the user within the group.
# For example, if the group has been given DEVELOPER access but the member has MASTER access,
# the user should receive only DEVELOPER access.
def max_invited_level_for_users(group_link, user_ids)
invited_group = group_link.group
capped_access_level = group_link.group_access
access = invited_group.group_members.access_for_user_ids(user_ids)
# If the user is not in the list, assume he/she does not have access
missing_users = user_ids - access.keys
missing_users.each { |id| access[id] = Gitlab::Access::NO_ACCESS }
# Cap the maximum access by the invited level access
access.each { |key, value| access[key] = [value, capped_access_level].min }
end
def fetch_members(level = nil)
project_members = project.members
group_members = group ? group.members : []
if level
project_members = project_members.public_send(level)
group_members = group_members.public_send(level) if group
end
user_ids = project_members.pluck(:user_id)
invited_members = fetch_invited_members(level)
user_ids.push(*invited_members.map(&:user_id)) if invited_members.any?
members = project.authorized_users
members = members.where(project_authorizations: { access_level: level }) if level
user_ids.push(*group_members.pluck(:user_id)) if group
User.where(id: user_ids)
members
end
def group
project.group
end
def merge_max!(first_hash, second_hash)
first_hash.merge!(second_hash) { |_key, old, new| old > new ? old : new }
end
def project_shared_with_group?
project.invited_groups.any? && project.allowed_to_share_with_group?
end
def fetch_invited_members(level = nil)
invited_members = []
return invited_members unless project_shared_with_group?
project.project_group_links.includes(group: [:group_members]).each do |link|
invited_group_members = link.group.members
if level
numeric_level = GroupMember.access_level_roles[level.to_s.singularize.titleize]
# If we're asked for a level that's higher than the group's access,
# there's nothing left to do
next if numeric_level > link.group_access
# Make sure we include everyone _above_ the requested level as well
invited_group_members =
if numeric_level == link.group_access
invited_group_members.where("access_level >= ?", link.group_access)
else
invited_group_members.public_send(level)
end
end
invited_members << invited_group_members
end
invited_members.flatten.compact
end
end
This diff is collapsed.
......@@ -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)
......
......@@ -291,8 +291,12 @@ class User < ActiveRecord::Base
end
end
def find_by_username(username)
iwhere(username: username).take
end
def find_by_username!(username)
find_by!('lower(username) = ?', username.downcase)
iwhere(username: username).take!
end
def find_by_personal_access_token(token_string)
......@@ -926,7 +930,7 @@ class User < ActiveRecord::Base
# Returns a union query of projects that the user is authorized to access
def project_authorizations_union
relations = [
personal_projects.select("#{id} AS user_id, projects.id AS project_id, #{Gitlab::Access::OWNER} AS access_level"),
personal_projects.select("#{id} AS user_id, projects.id AS project_id, #{Gitlab::Access::MASTER} AS access_level"),
groups_projects.select_for_project_authorization,
projects.select_for_project_authorization,
groups.joins(:shared_projects).select_for_project_authorization
......
......@@ -13,7 +13,7 @@ class AnalyticsBuildEntity < Grape::Entity
end
expose :duration, as: :total_time do |build|
distance_of_time_as_hash(build[:duration].to_f)
distance_of_time_as_hash(build.duration.to_f)
end
expose :branch do
......
......@@ -2,7 +2,7 @@ module EntityDateHelper
include ActionView::Helpers::DateHelper
def interval_in_words(diff)
"#{distance_of_time_in_words(diff.to_f)} ago"
"#{distance_of_time_in_words(Time.now, diff)} ago"
end
# Converts seconds into a hash such as:
......
require_relative 'base_service'
##
# Branch can be deleted either by DeleteBranchService
# or by GitPushService.
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment