Commit 9a9d7ad9 authored by Rémy Coutable's avatar Rémy Coutable

Merge remote-tracking branch 'ce/master' into rc/ce-to-ee-friday

Signed-off-by: default avatarRémy Coutable <remy@rymai.me>
parents 0e519d55 46aadc5c
...@@ -8,7 +8,6 @@ ...@@ -8,7 +8,6 @@
"plugins": [ "plugins": [
["istanbul", { ["istanbul", {
"exclude": [ "exclude": [
"app/assets/javascripts/droplab/**/*",
"spec/javascripts/**/*" "spec/javascripts/**/*"
] ]
}], }],
......
...@@ -333,7 +333,7 @@ migration paths: ...@@ -333,7 +333,7 @@ migration paths:
- master@gitlab/gitlabhq - master@gitlab/gitlabhq
- master@gitlab/gitlab-ee - master@gitlab/gitlab-ee
script: script:
- git fetch origin v8.5.9 - git fetch origin v8.14.10
- git checkout -f FETCH_HEAD - git checkout -f FETCH_HEAD
- cp config/resque.yml.example config/resque.yml - cp config/resque.yml.example config/resque.yml
- sed -i 's/localhost/redis/g' config/resque.yml - sed -i 's/localhost/redis/g' config/resque.yml
......
...@@ -154,6 +154,9 @@ gem 'sidekiq-cron', '~> 0.4.4' ...@@ -154,6 +154,9 @@ gem 'sidekiq-cron', '~> 0.4.4'
gem 'redis-namespace', '~> 1.5.2' gem 'redis-namespace', '~> 1.5.2'
gem 'sidekiq-limit_fetch', '~> 3.4' gem 'sidekiq-limit_fetch', '~> 3.4'
# Cron Parser
gem 'rufus-scheduler', '~> 3.1.10'
# HTTP requests # HTTP requests
gem 'httparty', '~> 0.13.3' gem 'httparty', '~> 0.13.3'
......
...@@ -1032,6 +1032,7 @@ DEPENDENCIES ...@@ -1032,6 +1032,7 @@ DEPENDENCIES
rubocop-rspec (~> 1.12.0) rubocop-rspec (~> 1.12.0)
ruby-fogbugz (~> 0.2.1) ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2) ruby-prof (~> 0.16.2)
rufus-scheduler (~> 3.1.10)
rugged (~> 0.25.1.1) rugged (~> 0.25.1.1)
sanitize (~> 2.0) sanitize (~> 2.0)
sass-rails (~> 5.0.6) sass-rails (~> 5.0.6)
......
...@@ -33,7 +33,7 @@ core team members will mention this person. ...@@ -33,7 +33,7 @@ core team members will mention this person.
### Merge request coaching ### Merge request coaching
Several people from the [GitLab team][team] are helping community members to get Several people from the [GitLab team][team] are helping community members to get
their contributions accepted by meeting our [Definition of done](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#definition-of-done). their contributions accepted by meeting our [Definition of done][done].
What you can expect from them is described at https://about.gitlab.com/jobs/merge-request-coach/. What you can expect from them is described at https://about.gitlab.com/jobs/merge-request-coach/.
...@@ -64,6 +64,49 @@ Merge requests may still be merged into master during this period, ...@@ -64,6 +64,49 @@ Merge requests may still be merged into master during this period,
but they will go into the _next_ release, unless they are manually cherry-picked into the stable branch. but they will go into the _next_ release, unless they are manually cherry-picked into the stable branch.
By freezing the stable branches 2 weeks prior to a release, we reduce the risk of a last minute merge request potentially breaking things. By freezing the stable branches 2 weeks prior to a release, we reduce the risk of a last minute merge request potentially breaking things.
### Between the 1st and the 7th
These types of merge requests need special consideration:
* **Large features**: a large feature is one that is highlighted in the kick-off
and the release blogpost; typically this will have its own channel in Slack
and a dedicated team with front-end, back-end, and UX.
* **Small features**: any other feature request.
**Large features** must be with a maintainer **by the 1st**. It's OK if they
aren't completely done, but this allows the maintainer enough time to make the
decision about whether this can make it in before the freeze. If the maintainer
doesn't think it will make it, they should inform the developers working on it
and the Product Manager responsible for the feature.
**Small features** must be with a reviewer (not necessarily maintainer) **by the
3rd**.
Most merge requests from the community do not have a specific release
target. However, if one does and falls into either of the above categories, it's
the reviewer's responsibility to manage the above communication and assignment
on behalf of the community member.
### On the 7th
Merge requests should still be complete, following the
[definition of done][done]. The single exception is documentation, and this can
only be left until after the freeze if:
* There is a follow-up issue to add documentation.
* It is assigned to the person writing documentation for this feature, and they
are aware of it.
* It is in the correct milestone, with the ~Deliverable label.
All Community Edition merge requests from GitLab team members merged on the
freeze date (the 7th) should have a corresponding Enterprise Edition merge
request, even if there are no conflicts. This is to reduce the size of the
subsequent EE merge, as we often merge a lot to CE on the release date. For more
information, see
[limit conflicts with EE when developing on CE][limit_ee_conflicts].
### Between the 7th and the 22nd
Once the stable branch is frozen, only fixes for regressions (bugs introduced in that same release) Once the stable branch is frozen, only fixes for regressions (bugs introduced in that same release)
and security issues will be cherry-picked into the stable branch. and security issues will be cherry-picked into the stable branch.
Any merge requests cherry-picked into the stable branch for a previous release will also be picked into the latest stable branch. Any merge requests cherry-picked into the stable branch for a previous release will also be picked into the latest stable branch.
...@@ -158,3 +201,5 @@ still an issue I encourage you to open it on the [GitLab.com issue tracker](http ...@@ -158,3 +201,5 @@ still an issue I encourage you to open it on the [GitLab.com issue tracker](http
[contribution acceptance criteria]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#contribution-acceptance-criteria [contribution acceptance criteria]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#contribution-acceptance-criteria
["Implement design & UI elements" guidelines]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#implement-design-ui-elements ["Implement design & UI elements" guidelines]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#implement-design-ui-elements
[Thoughtbot code review guide]: https://github.com/thoughtbot/guides/tree/master/code-review [Thoughtbot code review guide]: https://github.com/thoughtbot/guides/tree/master/code-review
[done]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#definition-of-done
[limit_ee_conflicts]: https://docs.gitlab.com/ce/development/limit_ee_conflicts.html
...@@ -51,7 +51,7 @@ function renderCategory(name, emojiList, opts = {}) { ...@@ -51,7 +51,7 @@ function renderCategory(name, emojiList, opts = {}) {
<h5 class="emoji-menu-title"> <h5 class="emoji-menu-title">
${name} ${name}
</h5> </h5>
<ul class="clearfix emoji-menu-list ${opts.menuListClass}"> <ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}">
${emojiList.map(emojiName => ` ${emojiList.map(emojiName => `
<li class="emoji-menu-list-item"> <li class="emoji-menu-list-item">
<button class="emoji-menu-btn text-center js-emoji-btn" type="button"> <button class="emoji-menu-btn text-center js-emoji-btn" type="button">
...@@ -263,7 +263,8 @@ AwardsHandler.prototype.addAward = function addAward( ...@@ -263,7 +263,8 @@ AwardsHandler.prototype.addAward = function addAward(
this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality); this.addAwardToEmojiBar(votesBlock, normalizedEmoji, checkMutuality);
return typeof callback === 'function' ? callback() : undefined; return typeof callback === 'function' ? callback() : undefined;
}); });
return $('.emoji-menu').removeClass('is-visible'); $('.emoji-menu').removeClass('is-visible');
$('.js-add-award.is-active').removeClass('is-active');
}; };
AwardsHandler.prototype.addAwardToEmojiBar = function addAwardToEmojiBar( AwardsHandler.prototype.addAwardToEmojiBar = function addAwardToEmojiBar(
......
...@@ -20,6 +20,7 @@ import eventHub from '../eventhub'; ...@@ -20,6 +20,7 @@ import eventHub from '../eventhub';
list: { list: {
type: Object, type: Object,
required: false, required: false,
default: () => ({}),
}, },
rootPath: { rootPath: {
type: String, type: String,
...@@ -31,6 +32,26 @@ import eventHub from '../eventhub'; ...@@ -31,6 +32,26 @@ import eventHub from '../eventhub';
default: false, default: false,
}, },
}, },
computed: {
cardUrl() {
return `${this.issueLinkBase}/${this.issue.id}`;
},
assigneeUrl() {
return `${this.rootPath}${this.issue.assignee.username}`;
},
assigneeUrlTitle() {
return `Assigned to ${this.issue.assignee.name}`;
},
avatarUrlTitle() {
return `Avatar for ${this.issue.assignee.name}`;
},
issueId() {
return `#${this.issue.id}`;
},
showLabelFooter() {
return this.issue.labels.find(l => this.showLabel(l)) !== undefined;
},
},
methods: { methods: {
showLabel(label) { showLabel(label) {
if (!this.list) return true; if (!this.list) return true;
...@@ -67,35 +88,41 @@ import eventHub from '../eventhub'; ...@@ -67,35 +88,41 @@ import eventHub from '../eventhub';
}, },
template: ` template: `
<div> <div>
<h4 class="card-title"> <div class="card-header">
<i <h4 class="card-title">
class="fa fa-eye-slash confidential-icon" <i
v-if="issue.confidential"></i> class="fa fa-eye-slash confidential-icon"
<a v-if="issue.confidential"
:href="issueLinkBase + '/' + issue.id" aria-hidden="true"
:title="issue.title"> />
{{ issue.title }} <a
</a> class="js-no-trigger"
</h4> :href="cardUrl"
<div class="card-footer"> :title="issue.title">{{ issue.title }}</a>
<span <span
class="card-number" class="card-number"
v-if="issue.id"> v-if="issue.id"
#{{ issue.id }} >
</span> {{ issueId }}
</span>
</h4>
<a <a
class="card-assignee has-tooltip js-no-trigger" class="card-assignee has-tooltip js-no-trigger"
:href="rootPath + issue.assignee.username" :href="assigneeUrl"
:title="'Assigned to ' + issue.assignee.name" :title="assigneeUrlTitle"
v-if="issue.assignee" v-if="issue.assignee"
data-container="body"> data-container="body"
>
<img <img
class="avatar avatar-inline s20 js-no-trigger" class="avatar avatar-inline s20 js-no-trigger"
:src="issue.assignee.avatar" :src="issue.assignee.avatar"
width="20" width="20"
height="20" height="20"
:alt="'Avatar for ' + issue.assignee.name" /> :alt="avatarUrlTitle"
/>
</a> </a>
</div>
<div class="card-footer" v-if="showLabelFooter">
<button <button
class="label color-label has-tooltip js-no-trigger" class="label color-label has-tooltip js-no-trigger"
v-for="label in issue.labels" v-for="label in issue.labels"
......
This diff is collapsed.
...@@ -8,10 +8,8 @@ Vue.use(VueResource); ...@@ -8,10 +8,8 @@ Vue.use(VueResource);
/** /**
* Commits View > Pipelines Tab > Pipelines Table. * Commits View > Pipelines Tab > Pipelines Table.
* Merge Request View > Pipelines Tab > Pipelines Table.
* *
* Renders Pipelines table in pipelines tab in the commits show view. * Renders Pipelines table in pipelines tab in the commits show view.
* Renders Pipelines table in pipelines tab in the merge request show view.
*/ */
$(() => { $(() => {
...@@ -20,13 +18,14 @@ $(() => { ...@@ -20,13 +18,14 @@ $(() => {
gl.commits.pipelines = gl.commits.pipelines || {}; gl.commits.pipelines = gl.commits.pipelines || {};
if (gl.commits.PipelinesTableBundle) { if (gl.commits.PipelinesTableBundle) {
document.querySelector('#commit-pipeline-table-view').removeChild(this.pipelinesTableBundle.$el);
gl.commits.PipelinesTableBundle.$destroy(true); gl.commits.PipelinesTableBundle.$destroy(true);
} }
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable();
if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) { if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) {
gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl); gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable().$mount();
document.querySelector('#commit-pipeline-table-view').appendChild(gl.commits.pipelines.PipelinesTableBundle.$el);
} }
}); });
import Vue from 'vue'; import Vue from 'vue';
import Visibility from 'visibilityjs';
import PipelinesTableComponent from '../../vue_shared/components/pipelines_table'; import PipelinesTableComponent from '../../vue_shared/components/pipelines_table';
import PipelinesService from '../../vue_pipelines_index/services/pipelines_service'; import PipelinesService from '../../vue_pipelines_index/services/pipelines_service';
import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store'; import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store';
...@@ -7,6 +8,7 @@ import EmptyState from '../../vue_pipelines_index/components/empty_state'; ...@@ -7,6 +8,7 @@ import EmptyState from '../../vue_pipelines_index/components/empty_state';
import ErrorState from '../../vue_pipelines_index/components/error_state'; import ErrorState from '../../vue_pipelines_index/components/error_state';
import '../../lib/utils/common_utils'; import '../../lib/utils/common_utils';
import '../../vue_shared/vue_resource_interceptor'; import '../../vue_shared/vue_resource_interceptor';
import Poll from '../../lib/utils/poll';
/** /**
* *
...@@ -20,6 +22,7 @@ import '../../vue_shared/vue_resource_interceptor'; ...@@ -20,6 +22,7 @@ import '../../vue_shared/vue_resource_interceptor';
*/ */
export default Vue.component('pipelines-table', { export default Vue.component('pipelines-table', {
components: { components: {
'pipelines-table-component': PipelinesTableComponent, 'pipelines-table-component': PipelinesTableComponent,
'error-state': ErrorState, 'error-state': ErrorState,
...@@ -42,6 +45,7 @@ export default Vue.component('pipelines-table', { ...@@ -42,6 +45,7 @@ export default Vue.component('pipelines-table', {
state: store.state, state: store.state,
isLoading: false, isLoading: false,
hasError: false, hasError: false,
isMakingRequest: false,
}; };
}, },
...@@ -64,17 +68,41 @@ export default Vue.component('pipelines-table', { ...@@ -64,17 +68,41 @@ export default Vue.component('pipelines-table', {
* *
*/ */
beforeMount() { beforeMount() {
this.endpoint = this.$el.dataset.endpoint; const element = document.querySelector('#commit-pipeline-table-view');
this.helpPagePath = this.$el.dataset.helpPagePath;
this.endpoint = element.dataset.endpoint;
this.helpPagePath = element.dataset.helpPagePath;
this.service = new PipelinesService(this.endpoint); this.service = new PipelinesService(this.endpoint);
this.fetchPipelines(); this.poll = new Poll({
resource: this.service,
method: 'getPipelines',
successCallback: this.successCallback,
errorCallback: this.errorCallback,
notificationCallback: this.setIsMakingRequest,
});
if (!Visibility.hidden()) {
this.isLoading = true;
this.poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
eventHub.$on('refreshPipelines', this.fetchPipelines); eventHub.$on('refreshPipelines', this.fetchPipelines);
}, },
beforeUpdate() { beforeUpdate() {
if (this.state.pipelines.length && this.$children) { if (this.state.pipelines.length &&
this.$children &&
!this.isMakingRequest &&
!this.isLoading) {
this.store.startTimeAgoLoops.call(this, Vue); this.store.startTimeAgoLoops.call(this, Vue);
} }
}, },
...@@ -83,21 +111,35 @@ export default Vue.component('pipelines-table', { ...@@ -83,21 +111,35 @@ export default Vue.component('pipelines-table', {
eventHub.$off('refreshPipelines'); eventHub.$off('refreshPipelines');
}, },
destroyed() {
this.poll.stop();
},
methods: { methods: {
fetchPipelines() { fetchPipelines() {
this.isLoading = true; this.isLoading = true;
return this.service.getPipelines() return this.service.getPipelines()
.then(response => response.json()) .then(response => this.successCallback(response))
.then((json) => { .catch(() => this.errorCallback());
// depending of the endpoint the response can either bring a `pipelines` key or not. },
const pipelines = json.pipelines || json;
this.store.storePipelines(pipelines); successCallback(resp) {
this.isLoading = false; const response = resp.json();
})
.catch(() => { // depending of the endpoint the response can either bring a `pipelines` key or not.
this.hasError = true; const pipelines = response.pipelines || response;
this.isLoading = false; this.store.storePipelines(pipelines);
}); this.isLoading = false;
},
errorCallback() {
this.hasError = true;
this.isLoading = false;
},
setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest;
}, },
}, },
......
...@@ -38,9 +38,35 @@ showTooltip = function(target, title) { ...@@ -38,9 +38,35 @@ showTooltip = function(target, title) {
}; };
$(function() { $(function() {
var clipboard; const clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]');
clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]');
clipboard.on('success', genericSuccess); clipboard.on('success', genericSuccess);
return clipboard.on('error', genericError); clipboard.on('error', genericError);
// This a workaround around ClipboardJS limitations to allow the context-specific copy/pasting of plain text or GFM.
// The Ruby `clipboard_button` helper sneaks a JSON hash with `text` and `gfm` keys into the `data-clipboard-text`
// attribute that ClipboardJS reads from.
// When ClipboardJS creates a new `textarea` (directly inside `body`, with a `readonly` attribute`), sets its value
// to the value of this data attribute, focusses on it, and finally programmatically issues the 'Copy' command,
// this code intercepts the copy command/event at the last minute to deconstruct this JSON hash and set the
// `text/plain` and `text/x-gfm` copy data types to the intended values.
$(document).on('copy', 'body > textarea[readonly]', function(e) {
const clipboardData = e.originalEvent.clipboardData;
if (!clipboardData) return;
const text = e.target.value;
let json;
try {
json = JSON.parse(text);
} catch (ex) {
return;
}
if (!json.text || !json.gfm) return;
e.preventDefault();
clipboardData.setData('text/plain', json.text);
clipboardData.setData('text/x-gfm', json.gfm);
});
}); });
...@@ -13,10 +13,6 @@ class Diff { ...@@ -13,10 +13,6 @@ class Diff {
$diffFile.each((index, file) => new gl.ImageFile(file)); $diffFile.each((index, file) => new gl.ImageFile(file));
if (this.diffViewType() === 'parallel') {
$('.content-wrapper .container-fluid').removeClass('container-limited');
}
if (!isBound) { if (!isBound) {
$(document) $(document)
.on('click', '.js-unfold', this.handleClickUnfold.bind(this)) .on('click', '.js-unfold', this.handleClickUnfold.bind(this))
......
...@@ -24,7 +24,6 @@ ...@@ -24,7 +24,6 @@
/* global Search */ /* global Search */
/* global Admin */ /* global Admin */
/* global NamespaceSelects */ /* global NamespaceSelects */
/* global ShortcutsDashboardNavigation */
/* global Project */ /* global Project */
/* global ProjectAvatar */ /* global ProjectAvatar */
/* global CompareAutocomplete */ /* global CompareAutocomplete */
...@@ -48,9 +47,12 @@ import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater'; ...@@ -48,9 +47,12 @@ import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
import BlobForkSuggestion from './blob/blob_fork_suggestion'; import BlobForkSuggestion from './blob/blob_fork_suggestion';
import UserCallout from './user_callout'; import UserCallout from './user_callout';
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags'; import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
<<<<<<< HEAD
import GeoNodes from './geo_nodes'; import GeoNodes from './geo_nodes';
import ServiceDeskRoot from './projects/settings_service_desk/service_desk_root'; import ServiceDeskRoot from './projects/settings_service_desk/service_desk_root';
=======
>>>>>>> ce/master
const ShortcutsBlob = require('./shortcuts_blob'); const ShortcutsBlob = require('./shortcuts_blob');
...@@ -354,7 +356,10 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -354,7 +356,10 @@ const ShortcutsBlob = require('./shortcuts_blob');
// Initialize Protected Branch Settings // Initialize Protected Branch Settings
new gl.ProtectedBranchCreate(); new gl.ProtectedBranchCreate();
new gl.ProtectedBranchEditList(); new gl.ProtectedBranchEditList();
<<<<<<< HEAD
new UsersSelect(); new UsersSelect();
=======
>>>>>>> ce/master
// Initialize Protected Tag Settings // Initialize Protected Tag Settings
new ProtectedTagCreate(); new ProtectedTagCreate();
new ProtectedTagEditList(); new ProtectedTagEditList();
...@@ -410,7 +415,6 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -410,7 +415,6 @@ const ShortcutsBlob = require('./shortcuts_blob');
break; break;
case 'dashboard': case 'dashboard':
case 'root': case 'root':
shortcut_handler = new ShortcutsDashboardNavigation();
new UserCallout(); new UserCallout();
break; break;
case 'groups': case 'groups':
......
const DATA_TRIGGER = 'data-dropdown-trigger';
const DATA_DROPDOWN = 'data-dropdown';
const SELECTED_CLASS = 'droplab-item-selected';
const ACTIVE_CLASS = 'droplab-item-active';
export {
DATA_TRIGGER,
DATA_DROPDOWN,
SELECTED_CLASS,
ACTIVE_CLASS,
};
/* eslint-disable */
import utils from './utils';
import { SELECTED_CLASS } from './constants';
var DropDown = function(list) {
this.currentIndex = 0;
this.hidden = true;
this.list = typeof list === 'string' ? document.querySelector(list) : list;
this.items = [];
this.eventWrapper = {};
this.getItems();
this.initTemplateString();
this.addEvents();
this.initialState = list.innerHTML;
};
Object.assign(DropDown.prototype, {
getItems: function() {
this.items = [].slice.call(this.list.querySelectorAll('li'));
return this.items;
},
initTemplateString: function() {
var items = this.items || this.getItems();
var templateString = '';
if (items.length > 0) templateString = items[items.length - 1].outerHTML;
this.templateString = templateString;
return this.templateString;
},
clickEvent: function(e) {
var selected = utils.closest(e.target, 'LI');
if (!selected) return;
this.addSelectedClass(selected);
e.preventDefault();
this.hide();
var listEvent = new CustomEvent('click.dl', {
detail: {
list: this,
selected: selected,
data: e.target.dataset,
},
});
this.list.dispatchEvent(listEvent);
},
addSelectedClass: function (selected) {
this.removeSelectedClasses();
selected.classList.add(SELECTED_CLASS);
},
removeSelectedClasses: function () {
const items = this.items || this.getItems();
items.forEach(item => item.classList.remove(SELECTED_CLASS));
},
addEvents: function() {
this.eventWrapper.clickEvent = this.clickEvent.bind(this)
this.list.addEventListener('click', this.eventWrapper.clickEvent);
},
toggle: function() {
this.hidden ? this.show() : this.hide();
},
setData: function(data) {
this.data = data;
this.render(data);
},
addData: function(data) {
this.data = (this.data || []).concat(data);
this.render(this.data);
},
render: function(data) {
const children = data ? data.map(this.renderChildren.bind(this)) : [];
const renderableList = this.list.querySelector('ul[data-dynamic]') || this.list;
renderableList.innerHTML = children.join('');
},
renderChildren: function(data) {
var html = utils.t(this.templateString, data);
var template = document.createElement('div');
template.innerHTML = html;
this.setImagesSrc(template);
template.firstChild.style.display = data.droplab_hidden ? 'none' : 'block';
return template.firstChild.outerHTML;
},
setImagesSrc: function(template) {
const images = [].slice.call(template.querySelectorAll('img[data-src]'));
images.forEach((image) => {
image.src = image.getAttribute('data-src');
image.removeAttribute('data-src');
});
},
show: function() {
if (!this.hidden) return;
this.list.style.display = 'block';
this.currentIndex = 0;
this.hidden = false;
},
hide: function() {
if (this.hidden) return;
this.list.style.display = 'none';
this.currentIndex = 0;
this.hidden = true;
},
toggle: function () {
this.hidden ? this.show() : this.hide();
},
destroy: function() {
this.hide();
this.list.removeEventListener('click', this.eventWrapper.clickEvent);
}
});
export default DropDown;
/* eslint-disable */
import HookButton from './hook_button';
import HookInput from './hook_input';
import utils from './utils';
import Keyboard from './keyboard';
import { DATA_TRIGGER } from './constants';
var DropLab = function() {
this.ready = false;
this.hooks = [];
this.queuedData = [];
this.config = {};
this.eventWrapper = {};
};
Object.assign(DropLab.prototype, {
loadStatic: function(){
var dropdownTriggers = [].slice.apply(document.querySelectorAll(`[${DATA_TRIGGER}]`));
this.addHooks(dropdownTriggers);
},
addData: function () {
var args = [].slice.apply(arguments);
this.applyArgs(args, '_addData');
},
setData: function() {
var args = [].slice.apply(arguments);
this.applyArgs(args, '_setData');
},
destroy: function() {
this.hooks.forEach(hook => hook.destroy());
this.hooks = [];
this.removeEvents();
},
applyArgs: function(args, methodName) {
if (this.ready) return this[methodName].apply(this, args);
this.queuedData = this.queuedData || [];
this.queuedData.push(args);
},
_addData: function(trigger, data) {
this._processData(trigger, data, 'addData');
},
_setData: function(trigger, data) {
this._processData(trigger, data, 'setData');
},
_processData: function(trigger, data, methodName) {
this.hooks.forEach((hook) => {
if (Array.isArray(trigger)) hook.list[methodName](trigger);
if (hook.trigger.id === trigger) hook.list[methodName](data);
});
},
addEvents: function() {
this.eventWrapper.documentClicked = this.documentClicked.bind(this)
document.addEventListener('click', this.eventWrapper.documentClicked);
},
documentClicked: function(e) {
let thisTag = e.target;
if (thisTag.tagName !== 'UL') thisTag = utils.closest(thisTag, 'UL');
if (utils.isDropDownParts(thisTag, this.hooks) || utils.isDropDownParts(e.target, this.hooks)) return;
this.hooks.forEach(hook => hook.list.hide());
},
removeEvents: function(){
document.removeEventListener('click', this.eventWrapper.documentClicked);
},
changeHookList: function(trigger, list, plugins, config) {
const availableTrigger = typeof trigger === 'string' ? document.getElementById(trigger) : trigger;
this.hooks.forEach((hook, i) => {
hook.list.list.dataset.dropdownActive = false;
if (hook.trigger !== availableTrigger) return;
hook.destroy();
this.hooks.splice(i, 1);
this.addHook(availableTrigger, list, plugins, config);
});
},
addHook: function(hook, list, plugins, config) {
const availableHook = typeof hook === 'string' ? document.querySelector(hook) : hook;
let availableList;
if (typeof list === 'string') {
availableList = document.querySelector(list);
} else if (list instanceof Element) {
availableList = list;
} else {
availableList = document.querySelector(hook.dataset[utils.toCamelCase(DATA_TRIGGER)]);
}
availableList.dataset.dropdownActive = true;
const HookObject = availableHook.tagName === 'INPUT' ? HookInput : HookButton;
this.hooks.push(new HookObject(availableHook, availableList, plugins, config));
return this;
},
addHooks: function(hooks, plugins, config) {
hooks.forEach(hook => this.addHook(hook, null, plugins, config));
return this;
},
setConfig: function(obj){
this.config = obj;
},
fireReady: function() {
const readyEvent = new CustomEvent('ready.dl', {
detail: {
dropdown: this,
},
});
document.dispatchEvent(readyEvent);
this.ready = true;
},
init: function (hook, list, plugins, config) {
hook ? this.addHook(hook, list, plugins, config) : this.loadStatic();
this.addEvents();
Keyboard();
this.fireReady();
this.queuedData.forEach(data => this.addData(data));
this.queuedData = [];
return this;
},
});
export default DropLab;
This diff is collapsed.
/* eslint-disable */
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.ajax||(g.ajax = {}));g=(g.datasource||(g.datasource = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
/* global droplab */
require('../window')(function(w){
function droplabAjaxException(message) {
this.message = message;
}
w.droplabAjax = {
_loadUrlData: function _loadUrlData(url) {
var self = this;
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest;
xhr.open('GET', url, true);
xhr.onreadystatechange = function () {
if(xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
var data = JSON.parse(xhr.responseText);
self.cache[url] = data;
return resolve(data);
} else {
return reject([xhr.responseText, xhr.status]);
}
}
};
xhr.send();
});
},
_loadData: function _loadData(data, config, self) {
if (config.loadingTemplate) {
var dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]');
if (dataLoadingTemplate) {
dataLoadingTemplate.outerHTML = self.listTemplate;
}
}
if (!self.destroyed) {
self.hook.list[config.method].call(self.hook.list, data);
}
},
init: function init(hook) {
var self = this;
self.destroyed = false;
self.cache = self.cache || {};
var config = hook.config.droplabAjax;
this.hook = hook;
if (!config || !config.endpoint || !config.method) {
return;
}
if (config.method !== 'setData' && config.method !== 'addData') {
return;
}
if (config.loadingTemplate) {
var dynamicList = hook.list.list.querySelector('[data-dynamic]');
var loadingTemplate = document.createElement('div');
loadingTemplate.innerHTML = config.loadingTemplate;
loadingTemplate.setAttribute('data-loading-template', '');
this.listTemplate = dynamicList.outerHTML;
dynamicList.outerHTML = loadingTemplate.outerHTML;
}
if (self.cache[config.endpoint]) {
self._loadData(self.cache[config.endpoint], config, self);
} else {
this._loadUrlData(config.endpoint)
.then(function(d) {
self._loadData(d, config, self);
}, function(xhrError) {
// TODO: properly handle errors due to XHR cancellation
return;
}).catch(function(e) {
throw new droplabAjaxException(e.message || e);
});
}
},
destroy: function() {
var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
this.destroyed = true;
if (this.listTemplate && dynamicList) {
dynamicList.outerHTML = this.listTemplate;
}
}
};
});
},{"../window":2}],2:[function(require,module,exports){
module.exports = function(callback) {
return (function() {
callback(this);
}).call(null);
};
},{}]},{},[1])(1)
});
/* eslint-disable */
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.ajax||(g.ajax = {}));g=(g.datasource||(g.datasource = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
/* global droplab */
require('../window')(function(w){
w.droplabAjaxFilter = {
init: function(hook) {
this.destroyed = false;
this.hook = hook;
this.notLoading();
this.debounceTriggerWrapper = this.debounceTrigger.bind(this);
this.hook.trigger.addEventListener('keydown.dl', this.debounceTriggerWrapper);
this.hook.trigger.addEventListener('focus', this.debounceTriggerWrapper);
this.trigger(true);
},
notLoading: function notLoading() {
this.loading = false;
},
debounceTrigger: function debounceTrigger(e) {
var NON_CHARACTER_KEYS = [16, 17, 18, 20, 37, 38, 39, 40, 91, 93];
var invalidKeyPressed = NON_CHARACTER_KEYS.indexOf(e.detail.which || e.detail.keyCode) > -1;
var focusEvent = e.type === 'focus';
if (invalidKeyPressed || this.loading) {
return;
}
if (this.timeout) {
clearTimeout(this.timeout);
}
this.timeout = setTimeout(this.trigger.bind(this, focusEvent), 200);
},
trigger: function trigger(getEntireList) {
var config = this.hook.config.droplabAjaxFilter;
var searchValue = this.trigger.value;
if (!config || !config.endpoint || !config.searchKey) {
return;
}
if (config.searchValueFunction) {
searchValue = config.searchValueFunction();
}
if (config.loadingTemplate && this.hook.list.data === undefined ||
this.hook.list.data.length === 0) {
var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
var loadingTemplate = document.createElement('div');
loadingTemplate.innerHTML = config.loadingTemplate;
loadingTemplate.setAttribute('data-loading-template', true);
this.listTemplate = dynamicList.outerHTML;
dynamicList.outerHTML = loadingTemplate.outerHTML;
}
if (getEntireList) {
searchValue = '';
}
if (config.searchKey === searchValue) {
return this.list.show();
}
this.loading = true;
var params = config.params || {};
params[config.searchKey] = searchValue;
var self = this;
self.cache = self.cache || {};
var url = config.endpoint + this.buildParams(params);
var urlCachedData = self.cache[url];
if (urlCachedData) {
self._loadData(urlCachedData, config, self);
} else {
this._loadUrlData(url)
.then(function(data) {
self._loadData(data, config, self);
}, function(xhrError) {
// TODO: properly handle errors due to XHR cancellation
return;
});
}
},
_loadUrlData: function _loadUrlData(url) {
var self = this;
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest;
xhr.open('GET', url, true);
xhr.onreadystatechange = function () {
if(xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
var data = JSON.parse(xhr.responseText);
self.cache[url] = data;
return resolve(data);
} else {
return reject([xhr.responseText, xhr.status]);
}
}
};
xhr.send();
});
},
_loadData: function _loadData(data, config, self) {
if (config.loadingTemplate && self.hook.list.data === undefined ||
self.hook.list.data.length === 0) {
const dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]');
if (dataLoadingTemplate) {
dataLoadingTemplate.outerHTML = self.listTemplate;
}
}
if (!self.destroyed) {
var hookListChildren = self.hook.list.list.children;
var onlyDynamicList = hookListChildren.length === 1 && hookListChildren[0].hasAttribute('data-dynamic');
if (onlyDynamicList && data.length === 0) {
self.hook.list.hide();
}
self.hook.list.setData.call(self.hook.list, data);
}
self.notLoading();
self.hook.list.currentIndex = 0;
},
buildParams: function(params) {
if (!params) return '';
var paramsArray = Object.keys(params).map(function(param) {
return param + '=' + (params[param] || '');
});
return '?' + paramsArray.join('&');
},
destroy: function destroy() {
if (this.timeout) {
clearTimeout(this.timeout);
}
this.destroyed = true;
this.hook.trigger.removeEventListener('keydown.dl', this.debounceTriggerWrapper);
this.hook.trigger.removeEventListener('focus', this.debounceTriggerWrapper);
}
};
});
},{"../window":2}],2:[function(require,module,exports){
module.exports = function(callback) {
return (function() {
callback(this);
}).call(null);
};
},{}]},{},[1])(1)
});
/* eslint-disable */
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.filter||(g.filter = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
/* global droplab */
require('../window')(function(w){
w.droplabFilter = {
keydownWrapper: function(e){
var hiddenCount = 0;
var dataHiddenCount = 0;
var list = e.detail.hook.list;
var data = list.data;
var value = e.detail.hook.trigger.value.toLowerCase();
var config = e.detail.hook.config.droplabFilter;
var matches = [];
var filterFunction;
// will only work on dynamically set data
if(!data){
return;
}
if (config && config.filterFunction && typeof config.filterFunction === 'function') {
filterFunction = config.filterFunction;
} else {
filterFunction = function(o){
// cheap string search
o.droplab_hidden = o[config.template].toLowerCase().indexOf(value) === -1;
return o;
};
}
dataHiddenCount = data.filter(function(o) {
return !o.droplab_hidden;
}).length;
matches = data.map(function(o) {
return filterFunction(o, value);
});
hiddenCount = matches.filter(function(o) {
return !o.droplab_hidden;
}).length;
if (dataHiddenCount !== hiddenCount) {
list.render(matches);
list.currentIndex = 0;
}
},
init: function init(hookInput) {
var config = hookInput.config.droplabFilter;
if (!config || (!config.template && !config.filterFunction)) {
return;
}
this.hookInput = hookInput;
this.hookInput.trigger.addEventListener('keyup.dl', this.keydownWrapper);
this.hookInput.trigger.addEventListener('mousedown.dl', this.keydownWrapper);
},
destroy: function destroy(){
this.hookInput.trigger.removeEventListener('keyup.dl', this.keydownWrapper);
this.hookInput.trigger.removeEventListener('mousedown.dl', this.keydownWrapper);
}
};
});
},{"../window":2}],2:[function(require,module,exports){
module.exports = function(callback) {
return (function() {
callback(this);
}).call(null);
};
},{}]},{},[1])(1)
});
/* eslint-disable */
import DropDown from './drop_down';
var Hook = function(trigger, list, plugins, config){
this.trigger = trigger;
this.list = new DropDown(list);
this.type = 'Hook';
this.event = 'click';
this.plugins = plugins || [];
this.config = config || {};
this.id = trigger.id;
};
Object.assign(Hook.prototype, {
addEvents: function(){},
constructor: Hook,
});
export default Hook;
/* eslint-disable */
import Hook from './hook';
var HookButton = function(trigger, list, plugins, config) {
Hook.call(this, trigger, list, plugins, config);
this.type = 'button';
this.event = 'click';
this.eventWrapper = {};
this.addEvents();
this.addPlugins();
};
HookButton.prototype = Object.create(Hook.prototype);
Object.assign(HookButton.prototype, {
addPlugins: function() {
this.plugins.forEach(plugin => plugin.init(this));
},
clicked: function(e){
var buttonEvent = new CustomEvent('click.dl', {
detail: {
hook: this,
},
bubbles: true,
cancelable: true
});
e.target.dispatchEvent(buttonEvent);
this.list.toggle();
},
addEvents: function(){
this.eventWrapper.clicked = this.clicked.bind(this);
this.trigger.addEventListener('click', this.eventWrapper.clicked);
},
removeEvents: function(){
this.trigger.removeEventListener('click', this.eventWrapper.clicked);
},
restoreInitialState: function() {
this.list.list.innerHTML = this.list.initialState;
},
removePlugins: function() {
this.plugins.forEach(plugin => plugin.destroy());
},
destroy: function() {
this.restoreInitialState();
this.removeEvents();
this.removePlugins();
},
constructor: HookButton,
});
export default HookButton;
/* eslint-disable */
import Hook from './hook';
var HookInput = function(trigger, list, plugins, config) {
Hook.call(this, trigger, list, plugins, config);
this.type = 'input';
this.event = 'input';
this.eventWrapper = {};
this.addEvents();
this.addPlugins();
};
Object.assign(HookInput.prototype, {
addPlugins: function() {
this.plugins.forEach(plugin => plugin.init(this));
},
addEvents: function(){
this.eventWrapper.mousedown = this.mousedown.bind(this);
this.eventWrapper.input = this.input.bind(this);
this.eventWrapper.keyup = this.keyup.bind(this);
this.eventWrapper.keydown = this.keydown.bind(this);
this.trigger.addEventListener('mousedown', this.eventWrapper.mousedown);
this.trigger.addEventListener('input', this.eventWrapper.input);
this.trigger.addEventListener('keyup', this.eventWrapper.keyup);
this.trigger.addEventListener('keydown', this.eventWrapper.keydown);
},
removeEvents: function() {
this.hasRemovedEvents = true;
this.trigger.removeEventListener('mousedown', this.eventWrapper.mousedown);
this.trigger.removeEventListener('input', this.eventWrapper.input);
this.trigger.removeEventListener('keyup', this.eventWrapper.keyup);
this.trigger.removeEventListener('keydown', this.eventWrapper.keydown);
},
input: function(e) {
if(this.hasRemovedEvents) return;
this.list.show();
const inputEvent = new CustomEvent('input.dl', {
detail: {
hook: this,
text: e.target.value,
},
bubbles: true,
cancelable: true
});
e.target.dispatchEvent(inputEvent);
},
mousedown: function(e) {
if (this.hasRemovedEvents) return;
const mouseEvent = new CustomEvent('mousedown.dl', {
detail: {
hook: this,
text: e.target.value,
},
bubbles: true,
cancelable: true,
});
e.target.dispatchEvent(mouseEvent);
},
keyup: function(e) {
if (this.hasRemovedEvents) return;
this.keyEvent(e, 'keyup.dl');
},
keydown: function(e) {
if (this.hasRemovedEvents) return;
this.keyEvent(e, 'keydown.dl');
},
keyEvent: function(e, eventName) {
this.list.show();
const keyEvent = new CustomEvent(eventName, {
detail: {
hook: this,
text: e.target.value,
which: e.which,
key: e.key,
},
bubbles: true,
cancelable: true,
});
e.target.dispatchEvent(keyEvent);
},
restoreInitialState: function() {
this.list.list.innerHTML = this.list.initialState;
},
removePlugins: function() {
this.plugins.forEach(plugin => plugin.destroy());
},
destroy: function() {
this.restoreInitialState();
this.removeEvents();
this.removePlugins();
this.list.destroy();
}
});
export default HookInput;
/* eslint-disable */
import { ACTIVE_CLASS } from './constants';
const Keyboard = function () {
var currentKey;
var currentFocus;
var isUpArrow = false;
var isDownArrow = false;
var removeHighlight = function removeHighlight(list) {
var itemElements = Array.prototype.slice.call(list.list.querySelectorAll('li:not(.divider)'), 0);
var listItems = [];
for(var i = 0; i < itemElements.length; i++) {
var listItem = itemElements[i];
listItem.classList.remove(ACTIVE_CLASS);
if (listItem.style.display !== 'none') {
listItems.push(listItem);
}
}
return listItems;
};
var setMenuForArrows = function setMenuForArrows(list) {
var listItems = removeHighlight(list);
if(list.currentIndex>0){
if(!listItems[list.currentIndex-1]){
list.currentIndex = list.currentIndex-1;
}
if (listItems[list.currentIndex-1]) {
var el = listItems[list.currentIndex-1];
var filterDropdownEl = el.closest('.filter-dropdown');
el.classList.add(ACTIVE_CLASS);
if (filterDropdownEl) {
var filterDropdownBottom = filterDropdownEl.offsetHeight;
var elOffsetTop = el.offsetTop - 30;
if (elOffsetTop > filterDropdownBottom) {
filterDropdownEl.scrollTop = elOffsetTop - filterDropdownBottom;
}
}
}
}
};
var mousedown = function mousedown(e) {
var list = e.detail.hook.list;
removeHighlight(list);
list.show();
list.currentIndex = 0;
isUpArrow = false;
isDownArrow = false;
};
var selectItem = function selectItem(list) {
var listItems = removeHighlight(list);
var currentItem = listItems[list.currentIndex-1];
var listEvent = new CustomEvent('click.dl', {
detail: {
list: list,
selected: currentItem,
data: currentItem.dataset,
},
});
list.list.dispatchEvent(listEvent);
list.hide();
}
var keydown = function keydown(e){
var typedOn = e.target;
var list = e.detail.hook.list;
var currentIndex = list.currentIndex;
isUpArrow = false;
isDownArrow = false;
if(e.detail.which){
currentKey = e.detail.which;
if(currentKey === 13){
selectItem(e.detail.hook.list);
return;
}
if(currentKey === 38) {
isUpArrow = true;
}
if(currentKey === 40) {
isDownArrow = true;
}
} else if(e.detail.key) {
currentKey = e.detail.key;
if(currentKey === 'Enter'){
selectItem(e.detail.hook.list);
return;
}
if(currentKey === 'ArrowUp') {
isUpArrow = true;
}
if(currentKey === 'ArrowDown') {
isDownArrow = true;
}
}
if(isUpArrow){ currentIndex--; }
if(isDownArrow){ currentIndex++; }
if(currentIndex < 0){ currentIndex = 0; }
list.currentIndex = currentIndex;
setMenuForArrows(e.detail.hook.list);
};
document.addEventListener('mousedown.dl', mousedown);
document.addEventListener('keydown.dl', keydown);
};
export default Keyboard;
/* eslint-disable */
const Ajax = {
_loadUrlData: function _loadUrlData(url) {
var self = this;
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest;
xhr.open('GET', url, true);
xhr.onreadystatechange = function () {
if(xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
var data = JSON.parse(xhr.responseText);
self.cache[url] = data;
return resolve(data);
} else {
return reject([xhr.responseText, xhr.status]);
}
}
};
xhr.send();
});
},
_loadData: function _loadData(data, config, self) {
if (config.loadingTemplate) {
var dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]');
if (dataLoadingTemplate) dataLoadingTemplate.outerHTML = self.listTemplate;
}
if (!self.destroyed) self.hook.list[config.method].call(self.hook.list, data);
},
init: function init(hook) {
var self = this;
self.destroyed = false;
self.cache = self.cache || {};
var config = hook.config.Ajax;
this.hook = hook;
if (!config || !config.endpoint || !config.method) {
return;
}
if (config.method !== 'setData' && config.method !== 'addData') {
return;
}
if (config.loadingTemplate) {
var dynamicList = hook.list.list.querySelector('[data-dynamic]');
var loadingTemplate = document.createElement('div');
loadingTemplate.innerHTML = config.loadingTemplate;
loadingTemplate.setAttribute('data-loading-template', '');
this.listTemplate = dynamicList.outerHTML;
dynamicList.outerHTML = loadingTemplate.outerHTML;
}
if (self.cache[config.endpoint]) {
self._loadData(self.cache[config.endpoint], config, self);
} else {
this._loadUrlData(config.endpoint)
.then(function(d) {
self._loadData(d, config, self);
}, config.onError).catch(config.onError);
}
},
destroy: function() {
this.destroyed = true;
}
};
export default Ajax;
/* eslint-disable */
const AjaxFilter = {
init: function(hook) {
this.destroyed = false;
this.hook = hook;
this.notLoading();
this.eventWrapper = {};
this.eventWrapper.debounceTrigger = this.debounceTrigger.bind(this);
this.hook.trigger.addEventListener('keydown.dl', this.eventWrapper.debounceTrigger);
this.hook.trigger.addEventListener('focus', this.eventWrapper.debounceTrigger);
this.trigger(true);
},
notLoading: function notLoading() {
this.loading = false;
},
debounceTrigger: function debounceTrigger(e) {
var NON_CHARACTER_KEYS = [16, 17, 18, 20, 37, 38, 39, 40, 91, 93];
var invalidKeyPressed = NON_CHARACTER_KEYS.indexOf(e.detail.which || e.detail.keyCode) > -1;
var focusEvent = e.type === 'focus';
if (invalidKeyPressed || this.loading) {
return;
}
if (this.timeout) {
clearTimeout(this.timeout);
}
this.timeout = setTimeout(this.trigger.bind(this, focusEvent), 200);
},
trigger: function trigger(getEntireList) {
var config = this.hook.config.AjaxFilter;
var searchValue = this.trigger.value;
if (!config || !config.endpoint || !config.searchKey) {
return;
}
if (config.searchValueFunction) {
searchValue = config.searchValueFunction();
}
if (config.loadingTemplate && this.hook.list.data === undefined ||
this.hook.list.data.length === 0) {
var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
var loadingTemplate = document.createElement('div');
loadingTemplate.innerHTML = config.loadingTemplate;
loadingTemplate.setAttribute('data-loading-template', true);
this.listTemplate = dynamicList.outerHTML;
dynamicList.outerHTML = loadingTemplate.outerHTML;
}
if (getEntireList) {
searchValue = '';
}
if (config.searchKey === searchValue) {
return this.list.show();
}
this.loading = true;
var params = config.params || {};
params[config.searchKey] = searchValue;
var self = this;
self.cache = self.cache || {};
var url = config.endpoint + this.buildParams(params);
var urlCachedData = self.cache[url];
if (urlCachedData) {
self._loadData(urlCachedData, config, self);
} else {
this._loadUrlData(url)
.then(function(data) {
self._loadData(data, config, self);
}, config.onError).catch(config.onError);
}
},
_loadUrlData: function _loadUrlData(url) {
var self = this;
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest;
xhr.open('GET', url, true);
xhr.onreadystatechange = function () {
if(xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
var data = JSON.parse(xhr.responseText);
self.cache[url] = data;
return resolve(data);
} else {
return reject([xhr.responseText, xhr.status]);
}
}
};
xhr.send();
});
},
_loadData: function _loadData(data, config, self) {
const list = self.hook.list;
if (config.loadingTemplate && list.data === undefined ||
list.data.length === 0) {
const dataLoadingTemplate = list.list.querySelector('[data-loading-template]');
if (dataLoadingTemplate) {
dataLoadingTemplate.outerHTML = self.listTemplate;
}
}
if (!self.destroyed) {
var hookListChildren = list.list.children;
var onlyDynamicList = hookListChildren.length === 1 && hookListChildren[0].hasAttribute('data-dynamic');
if (onlyDynamicList && data.length === 0) {
list.hide();
}
list.setData.call(list, data);
}
self.notLoading();
list.currentIndex = 0;
},
buildParams: function(params) {
if (!params) return '';
var paramsArray = Object.keys(params).map(function(param) {
return param + '=' + (params[param] || '');
});
return '?' + paramsArray.join('&');
},
destroy: function destroy() {
if (this.timeout)clearTimeout(this.timeout);
this.destroyed = true;
this.hook.trigger.removeEventListener('keydown.dl', this.eventWrapper.debounceTrigger);
this.hook.trigger.removeEventListener('focus', this.eventWrapper.debounceTrigger);
}
};
export default AjaxFilter;
/* eslint-disable */
const Filter = {
keydown: function(e){
if (this.destroyed) return;
var hiddenCount = 0;
var dataHiddenCount = 0;
var list = e.detail.hook.list;
var data = list.data;
var value = e.detail.hook.trigger.value.toLowerCase();
var config = e.detail.hook.config.Filter;
var matches = [];
var filterFunction;
// will only work on dynamically set data
if(!data){
return;
}
if (config && config.filterFunction && typeof config.filterFunction === 'function') {
filterFunction = config.filterFunction;
} else {
filterFunction = function(o){
// cheap string search
o.droplab_hidden = o[config.template].toLowerCase().indexOf(value) === -1;
return o;
};
}
dataHiddenCount = data.filter(function(o) {
return !o.droplab_hidden;
}).length;
matches = data.map(function(o) {
return filterFunction(o, value);
});
hiddenCount = matches.filter(function(o) {
return !o.droplab_hidden;
}).length;
if (dataHiddenCount !== hiddenCount) {
list.setData(matches);
list.currentIndex = 0;
}
},
debounceKeydown: function debounceKeydown(e) {
if ([
13, // enter
16, // shift
17, // ctrl
18, // alt
20, // caps lock
37, // left arrow
38, // up arrow
39, // right arrow
40, // down arrow
91, // left window
92, // right window
93, // select
].indexOf(e.detail.which || e.detail.keyCode) > -1) return;
if (this.timeout) clearTimeout(this.timeout);
this.timeout = setTimeout(this.keydown.bind(this, e), 200);
},
init: function init(hook) {
var config = hook.config.Filter;
if (!config || !config.template) return;
this.hook = hook;
this.destroyed = false;
this.eventWrapper = {};
this.eventWrapper.debounceKeydown = this.debounceKeydown.bind(this);
this.hook.trigger.addEventListener('keydown.dl', this.eventWrapper.debounceKeydown);
this.hook.trigger.addEventListener('mousedown.dl', this.eventWrapper.debounceKeydown);
this.debounceKeydown({ detail: { hook: this.hook } });
},
destroy: function destroy() {
if (this.timeout) clearTimeout(this.timeout);
this.destroyed = true;
this.hook.trigger.removeEventListener('keydown.dl', this.eventWrapper.debounceKeydown);
this.hook.trigger.removeEventListener('mousedown.dl', this.eventWrapper.debounceKeydown);
}
};
export default Filter;
/* eslint-disable */
const InputSetter = {
init(hook) {
this.hook = hook;
this.destroyed = false;
this.config = hook.config.InputSetter || (this.hook.config.InputSetter = {});
this.eventWrapper = {};
this.addEvents();
},
addEvents() {
this.eventWrapper.setInputs = this.setInputs.bind(this);
this.hook.list.list.addEventListener('click.dl', this.eventWrapper.setInputs);
},
removeEvents() {
this.hook.list.list.removeEventListener('click.dl', this.eventWrapper.setInputs);
},
setInputs(e) {
if (this.destroyed) return;
const selectedItem = e.detail.selected;
if (!Array.isArray(this.config)) this.config = [this.config];
this.config.forEach(config => this.setInput(config, selectedItem));
},
setInput(config, selectedItem) {
const input = config.input || this.hook.trigger;
const newValue = selectedItem.getAttribute(config.valueAttribute);
const inputAttribute = config.inputAttribute;
if (!newValue) return;
if (input.hasAttribute(inputAttribute)) return input.setAttribute(inputAttribute, newValue);
if (input.tagName === 'INPUT') return input.value = newValue;
return input.textContent = newValue;
},
destroy() {
this.destroyed = true;
this.removeEvents();
},
};
export default InputSetter;
/* eslint-disable */
import { DATA_TRIGGER, DATA_DROPDOWN } from './constants';
const utils = {
toCamelCase(attr) {
return this.camelize(attr.split('-').slice(1).join(' '));
},
t(s, d) {
for (const p in d) {
if (Object.prototype.hasOwnProperty.call(d, p)) {
s = s.replace(new RegExp(`{{${p}}}`, 'g'), d[p]);
}
}
return s;
},
camelize(str) {
return str.replace(/(?:^\w|[A-Z]|\b\w)/g, (letter, index) => {
return index === 0 ? letter.toLowerCase() : letter.toUpperCase();
}).replace(/\s+/g, '');
},
closest(thisTag, stopTag) {
while (thisTag && thisTag.tagName !== stopTag && thisTag.tagName !== 'HTML') {
thisTag = thisTag.parentNode;
}
return thisTag;
},
isDropDownParts(target) {
if (!target || target.tagName === 'HTML') return false;
return target.hasAttribute(DATA_TRIGGER) || target.hasAttribute(DATA_DROPDOWN);
},
};
export default utils;
...@@ -32,12 +32,6 @@ export default Vue.component('environment-folder-view', { ...@@ -32,12 +32,6 @@ export default Vue.component('environment-folder-view', {
cssContainerClass: environmentsData.cssClass, cssContainerClass: environmentsData.cssClass,
canCreateDeployment: environmentsData.canCreateDeployment, canCreateDeployment: environmentsData.canCreateDeployment,
canReadEnvironment: environmentsData.canReadEnvironment, canReadEnvironment: environmentsData.canReadEnvironment,
// svgs
commitIconSvg: environmentsData.commitIconSvg,
playIconSvg: environmentsData.playIconSvg,
terminalIconSvg: environmentsData.terminalIconSvg,
// Pagination Properties, // Pagination Properties,
paginationInformation: {}, paginationInformation: {},
pageNumber: 1, pageNumber: 1,
...@@ -175,11 +169,14 @@ export default Vue.component('environment-folder-view', { ...@@ -175,11 +169,14 @@ export default Vue.component('environment-folder-view', {
:environments="state.environments" :environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed" :can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed" :can-read-environment="canReadEnvironmentParsed"
<<<<<<< HEAD
:play-icon-svg="playIconSvg" :play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg" :terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg" :commit-icon-svg="commitIconSvg"
:toggleDeployBoard="toggleDeployBoard" :toggleDeployBoard="toggleDeployBoard"
:store="store" :store="store"
=======
>>>>>>> ce/master
:service="service"/> :service="service"/>
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
......
require('./filtered_search_dropdown'); import Filter from '~/droplab/plugins/filter';
/* global droplabFilter */ require('./filtered_search_dropdown');
(() => { (() => {
class DropdownHint extends gl.FilteredSearchDropdown { class DropdownHint extends gl.FilteredSearchDropdown {
constructor(droplab, dropdown, input, filter) { constructor(droplab, dropdown, input, filter) {
super(droplab, dropdown, input, filter); super(droplab, dropdown, input, filter);
this.config = { this.config = {
droplabFilter: { Filter: {
template: 'hint', template: 'hint',
filterFunction: gl.DropdownUtils.filterHint.bind(null, input), filterFunction: gl.DropdownUtils.filterHint.bind(null, input),
}, },
...@@ -69,12 +69,12 @@ require('./filtered_search_dropdown'); ...@@ -69,12 +69,12 @@ require('./filtered_search_dropdown');
} }
}); });
this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config); this.droplab.changeHookList(this.hookId, this.dropdown, [Filter], this.config);
this.droplab.setData(this.hookId, dropdownData); this.droplab.setData(this.hookId, dropdownData);
} }
init() { init() {
this.droplab.addHook(this.input, this.dropdown, [droplabFilter], this.config).init(); this.droplab.addHook(this.input, this.dropdown, [Filter], this.config).init();
} }
} }
......
require('./filtered_search_dropdown'); /* global Flash */
import Ajax from '~/droplab/plugins/ajax';
import Filter from '~/droplab/plugins/filter';
/* global droplabAjax */ require('./filtered_search_dropdown');
/* global droplabFilter */
(() => { (() => {
class DropdownNonUser extends gl.FilteredSearchDropdown { class DropdownNonUser extends gl.FilteredSearchDropdown {
...@@ -9,13 +11,19 @@ require('./filtered_search_dropdown'); ...@@ -9,13 +11,19 @@ require('./filtered_search_dropdown');
super(droplab, dropdown, input, filter); super(droplab, dropdown, input, filter);
this.symbol = symbol; this.symbol = symbol;
this.config = { this.config = {
droplabAjax: { Ajax: {
endpoint, endpoint,
method: 'setData', method: 'setData',
loadingTemplate: this.loadingTemplate, loadingTemplate: this.loadingTemplate,
onError() {
/* eslint-disable no-new */
new Flash('An error occured fetching the dropdown data.');
/* eslint-enable no-new */
},
}, },
droplabFilter: { Filter: {
filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input), filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol, input),
template: 'title',
}, },
}; };
} }
...@@ -29,13 +37,13 @@ require('./filtered_search_dropdown'); ...@@ -29,13 +37,13 @@ require('./filtered_search_dropdown');
renderContent(forceShowList = false) { renderContent(forceShowList = false) {
this.droplab this.droplab
.changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config); .changeHookList(this.hookId, this.dropdown, [Ajax, Filter], this.config);
super.renderContent(forceShowList); super.renderContent(forceShowList);
} }
init() { init() {
this.droplab this.droplab
.addHook(this.input, this.dropdown, [droplabAjax, droplabFilter], this.config).init(); .addHook(this.input, this.dropdown, [Ajax, Filter], this.config).init();
} }
} }
......
require('./filtered_search_dropdown'); /* global Flash */
import AjaxFilter from '~/droplab/plugins/ajax_filter';
/* global droplabAjaxFilter */ require('./filtered_search_dropdown');
(() => { (() => {
class DropdownUser extends gl.FilteredSearchDropdown { class DropdownUser extends gl.FilteredSearchDropdown {
constructor(droplab, dropdown, input, filter) { constructor(droplab, dropdown, input, filter) {
super(droplab, dropdown, input, filter); super(droplab, dropdown, input, filter);
this.config = { this.config = {
droplabAjaxFilter: { AjaxFilter: {
endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`, endpoint: `${gon.relative_url_root || ''}/autocomplete/users.json`,
searchKey: 'search', searchKey: 'search',
params: { params: {
...@@ -18,6 +20,11 @@ require('./filtered_search_dropdown'); ...@@ -18,6 +20,11 @@ require('./filtered_search_dropdown');
}, },
searchValueFunction: this.getSearchInput.bind(this), searchValueFunction: this.getSearchInput.bind(this),
loadingTemplate: this.loadingTemplate, loadingTemplate: this.loadingTemplate,
onError() {
/* eslint-disable no-new */
new Flash('An error occured fetching the dropdown data.');
/* eslint-enable no-new */
},
}, },
}; };
} }
...@@ -28,7 +35,7 @@ require('./filtered_search_dropdown'); ...@@ -28,7 +35,7 @@ require('./filtered_search_dropdown');
} }
renderContent(forceShowList = false) { renderContent(forceShowList = false) {
this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjaxFilter], this.config); this.droplab.changeHookList(this.hookId, this.dropdown, [AjaxFilter], this.config);
super.renderContent(forceShowList); super.renderContent(forceShowList);
} }
...@@ -56,7 +63,7 @@ require('./filtered_search_dropdown'); ...@@ -56,7 +63,7 @@ require('./filtered_search_dropdown');
} }
init() { init() {
this.droplab.addHook(this.input, this.dropdown, [droplabAjaxFilter], this.config).init(); this.droplab.addHook(this.input, this.dropdown, [AjaxFilter], this.config).init();
} }
} }
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
class FilteredSearchDropdown { class FilteredSearchDropdown {
constructor(droplab, dropdown, input, filter) { constructor(droplab, dropdown, input, filter) {
this.droplab = droplab; this.droplab = droplab;
this.hookId = input && input.getAttribute('data-id'); this.hookId = input && input.id;
this.input = input; this.input = input;
this.filter = filter; this.filter = filter;
this.dropdown = dropdown; this.dropdown = dropdown;
......
/* global DropLab */ import DropLab from '~/droplab/drop_lab';
import FilteredSearchContainer from './container'; import FilteredSearchContainer from './container';
(() => { (() => {
......
...@@ -16,10 +16,13 @@ import eventHub from './event_hub'; ...@@ -16,10 +16,13 @@ import eventHub from './event_hub';
this.tokensContainer = this.container.querySelector('.tokens-container'); this.tokensContainer = this.container.querySelector('.tokens-container');
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys; this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeys;
<<<<<<< HEAD
if (page === 'issues' || page === 'boards') { if (page === 'issues' || page === 'boards') {
this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeysWithWeights; this.filteredSearchTokenKeys = gl.FilteredSearchTokenKeysWithWeights;
} }
=======
>>>>>>> ce/master
this.recentSearchesStore = new RecentSearchesStore(); this.recentSearchesStore = new RecentSearchesStore();
let recentSearchesKey = 'issue-recent-searches'; let recentSearchesKey = 'issue-recent-searches';
if (page === 'merge_requests') { if (page === 'merge_requests') {
...@@ -160,7 +163,7 @@ import eventHub from './event_hub'; ...@@ -160,7 +163,7 @@ import eventHub from './event_hub';
if (e.keyCode === 13) { if (e.keyCode === 13) {
const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown]; const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
const dropdownEl = dropdown.element; const dropdownEl = dropdown.element;
const activeElements = dropdownEl.querySelectorAll('.dropdown-active'); const activeElements = dropdownEl.querySelectorAll('.droplab-item-active');
e.preventDefault(); e.preventDefault();
......
...@@ -65,7 +65,6 @@ export default class Poll { ...@@ -65,7 +65,6 @@ export default class Poll {
this.makeRequest(); this.makeRequest();
}, pollInterval); }, pollInterval);
} }
this.options.successCallback(response); this.options.successCallback(response);
} }
...@@ -76,8 +75,14 @@ export default class Poll { ...@@ -76,8 +75,14 @@ export default class Poll {
notificationCallback(true); notificationCallback(true);
return resource[method](data) return resource[method](data)
.then(response => this.checkConditions(response)) .then((response) => {
.catch(error => errorCallback(error)); this.checkConditions(response);
notificationCallback(false);
})
.catch((error) => {
notificationCallback(false);
errorCallback(error);
});
} }
/** /**
......
...@@ -75,12 +75,6 @@ import './u2f/error'; ...@@ -75,12 +75,6 @@ import './u2f/error';
import './u2f/register'; import './u2f/register';
import './u2f/util'; import './u2f/util';
// droplab
import './droplab/droplab';
import './droplab/droplab_ajax';
import './droplab/droplab_ajax_filter';
import './droplab/droplab_filter';
// everything else // everything else
import './abuse_reports'; import './abuse_reports';
import './activities'; import './activities';
......
...@@ -90,6 +90,7 @@ import './flash'; ...@@ -90,6 +90,7 @@ import './flash';
.on('click', this.clickTab); .on('click', this.clickTab);
} }
// Used in tests
unbindEvents() { unbindEvents() {
$(document) $(document)
.off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown) .off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown)
...@@ -99,9 +100,11 @@ import './flash'; ...@@ -99,9 +100,11 @@ import './flash';
.off('click', this.clickTab); .off('click', this.clickTab);
} }
destroy() { destroyPipelinesView() {
this.unbindEvents();
if (this.commitPipelinesTable) { if (this.commitPipelinesTable) {
document.querySelector('#commit-pipeline-table-view')
.removeChild(this.commitPipelinesTable.$el);
this.commitPipelinesTable.$destroy(); this.commitPipelinesTable.$destroy();
} }
} }
...@@ -128,6 +131,7 @@ import './flash'; ...@@ -128,6 +131,7 @@ import './flash';
this.loadCommits($target.attr('href')); this.loadCommits($target.attr('href'));
this.expandView(); this.expandView();
this.resetViewContainer(); this.resetViewContainer();
this.destroyPipelinesView();
} else if (this.isDiffAction(action)) { } else if (this.isDiffAction(action)) {
this.loadDiff($target.attr('href')); this.loadDiff($target.attr('href'));
if (Breakpoints.get().getBreakpointSize() !== 'lg') { if (Breakpoints.get().getBreakpointSize() !== 'lg') {
...@@ -136,12 +140,14 @@ import './flash'; ...@@ -136,12 +140,14 @@ import './flash';
if (this.diffViewType() === 'parallel') { if (this.diffViewType() === 'parallel') {
this.expandViewContainer(); this.expandViewContainer();
} }
this.destroyPipelinesView();
} else if (action === 'pipelines') { } else if (action === 'pipelines') {
this.resetViewContainer(); this.resetViewContainer();
this.loadPipelines(); this.mountPipelinesView();
} else { } else {
this.expandView(); this.expandView();
this.resetViewContainer(); this.resetViewContainer();
this.destroyPipelinesView();
} }
if (this.setUrl) { if (this.setUrl) {
this.setCurrentAction(action); this.setCurrentAction(action);
...@@ -227,16 +233,12 @@ import './flash'; ...@@ -227,16 +233,12 @@ import './flash';
}); });
} }
loadPipelines() { mountPipelinesView() {
if (this.pipelinesLoaded) { this.commitPipelinesTable = new CommitPipelinesTable().$mount();
return; // $mount(el) replaces the el with the new rendered component. We need it in order to mount
} // it everytime this tab is clicked - https://vuejs.org/v2/api/#vm-mount
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); document.querySelector('#commit-pipeline-table-view')
// Could already be mounted from the `pipelines_bundle` .appendChild(this.commitPipelinesTable.$el);
if (pipelineTableViewEl) {
this.commitPipelinesTable = new CommitPipelinesTable().$mount(pipelineTableViewEl);
}
this.pipelinesLoaded = true;
} }
loadDiff(source) { loadDiff(source) {
......
...@@ -6,7 +6,10 @@ import statusCodes from '~/lib/utils/http_status'; ...@@ -6,7 +6,10 @@ import statusCodes from '~/lib/utils/http_status';
import { formatRelevantDigits } from '~/lib/utils/number_utils'; import { formatRelevantDigits } from '~/lib/utils/number_utils';
import '../flash'; import '../flash';
const prometheusContainer = '.prometheus-container';
const prometheusParentGraphContainer = '.prometheus-graphs';
const prometheusGraphsContainer = '.prometheus-graph'; const prometheusGraphsContainer = '.prometheus-graph';
const prometheusStatesContainer = '.prometheus-state';
const metricsEndpoint = 'metrics.json'; const metricsEndpoint = 'metrics.json';
const timeFormat = d3.time.format('%H:%M'); const timeFormat = d3.time.format('%H:%M');
const dayFormat = d3.time.format('%b %e, %a'); const dayFormat = d3.time.format('%b %e, %a');
...@@ -14,19 +17,30 @@ const bisectDate = d3.bisector(d => d.time).left; ...@@ -14,19 +17,30 @@ const bisectDate = d3.bisector(d => d.time).left;
const extraAddedWidthParent = 100; const extraAddedWidthParent = 100;
class PrometheusGraph { class PrometheusGraph {
constructor() { constructor() {
this.margin = { top: 80, right: 180, bottom: 80, left: 100 }; const $prometheusContainer = $(prometheusContainer);
this.marginLabelContainer = { top: 40, right: 0, bottom: 40, left: 0 }; const hasMetrics = $prometheusContainer.data('has-metrics');
const parentContainerWidth = $(prometheusGraphsContainer).parent().width() + this.docLink = $prometheusContainer.data('doc-link');
extraAddedWidthParent; this.integrationLink = $prometheusContainer.data('prometheus-integration');
this.originalWidth = parentContainerWidth;
this.originalHeight = 330; $(document).ajaxError(() => {});
this.width = parentContainerWidth - this.margin.left - this.margin.right;
this.height = this.originalHeight - this.margin.top - this.margin.bottom; if (hasMetrics) {
this.backOffRequestCounter = 0; this.margin = { top: 80, right: 180, bottom: 80, left: 100 };
this.configureGraph(); this.marginLabelContainer = { top: 40, right: 0, bottom: 40, left: 0 };
this.init(); const parentContainerWidth = $(prometheusGraphsContainer).parent().width() +
extraAddedWidthParent;
this.originalWidth = parentContainerWidth;
this.originalHeight = 330;
this.width = parentContainerWidth - this.margin.left - this.margin.right;
this.height = this.originalHeight - this.margin.top - this.margin.bottom;
this.backOffRequestCounter = 0;
this.configureGraph();
this.init();
} else {
this.state = '.js-getting-started';
this.updateState();
}
} }
createGraph() { createGraph() {
...@@ -40,8 +54,19 @@ class PrometheusGraph { ...@@ -40,8 +54,19 @@ class PrometheusGraph {
init() { init() {
this.getData().then((metricsResponse) => { this.getData().then((metricsResponse) => {
if (Object.keys(metricsResponse).length === 0) { let enoughData = true;
new Flash('Empty metrics', 'alert'); Object.keys(metricsResponse.metrics).forEach((key) => {
let currentKey;
if (key === 'cpu_values' || key === 'memory_values') {
currentKey = metricsResponse.metrics[key];
if (Object.keys(currentKey).length === 0) {
enoughData = false;
}
}
});
if (!enoughData) {
this.state = '.js-loading';
this.updateState();
} else { } else {
this.transformData(metricsResponse); this.transformData(metricsResponse);
this.createGraph(); this.createGraph();
...@@ -345,14 +370,17 @@ class PrometheusGraph { ...@@ -345,14 +370,17 @@ class PrometheusGraph {
} }
return resp.metrics; return resp.metrics;
}) })
.catch(() => new Flash('An error occurred while fetching metrics.', 'alert')); .catch(() => {
this.state = '.js-unable-to-connect';
this.updateState();
});
} }
transformData(metricsResponse) { transformData(metricsResponse) {
Object.keys(metricsResponse.metrics).forEach((key) => { Object.keys(metricsResponse.metrics).forEach((key) => {
if (key === 'cpu_values' || key === 'memory_values') { if (key === 'cpu_values' || key === 'memory_values') {
const metricValues = (metricsResponse.metrics[key])[0]; const metricValues = (metricsResponse.metrics[key])[0];
if (typeof metricValues !== 'undefined') { if (metricValues !== undefined) {
this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({ this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({
time: new Date(metric[0] * 1000), time: new Date(metric[0] * 1000),
value: metric[1], value: metric[1],
...@@ -361,6 +389,13 @@ class PrometheusGraph { ...@@ -361,6 +389,13 @@ class PrometheusGraph {
} }
}); });
} }
updateState() {
const $statesContainer = $(prometheusStatesContainer);
$(prometheusParentGraphContainer).hide();
$(`${this.state}`, $statesContainer).removeClass('hidden');
$(prometheusStatesContainer).show();
}
} }
export default PrometheusGraph; export default PrometheusGraph;
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, wrap-iife, quotes, prefer-arrow-callback, consistent-return, object-shorthand, no-unused-vars, one-var, one-var-declaration-per-line, no-else-return, comma-dangle, max-len */
/* global Mousetrap */ /* global Mousetrap */
/* global findFileURL */ /* global findFileURL */
import findAndFollowLink from './shortcuts_dashboard_navigation';
(function() { (function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
...@@ -14,11 +15,33 @@ ...@@ -14,11 +15,33 @@
} }
Mousetrap.bind('?', this.onToggleHelp); Mousetrap.bind('?', this.onToggleHelp);
Mousetrap.bind('s', Shortcuts.focusSearch); Mousetrap.bind('s', Shortcuts.focusSearch);
Mousetrap.bind('f', (function(_this) { Mousetrap.bind('f', (e => this.focusFilter(e)));
return function(e) {
return _this.focusFilter(e); const $globalDropdownMenu = $('.global-dropdown-menu');
}; const $globalDropdownToggle = $('.global-dropdown-toggle');
})(this));
$('.global-dropdown').on('hide.bs.dropdown', () => {
$globalDropdownMenu.removeClass('shortcuts');
});
Mousetrap.bind('n', () => {
$globalDropdownMenu.toggleClass('shortcuts');
$globalDropdownToggle.trigger('click');
if (!$globalDropdownMenu.is(':visible')) {
$globalDropdownToggle.blur();
}
});
Mousetrap.bind('shift+t', () => findAndFollowLink('.shortcuts-todos'));
Mousetrap.bind('shift+a', () => findAndFollowLink('.dashboard-shortcuts-activity'));
Mousetrap.bind('shift+i', () => findAndFollowLink('.dashboard-shortcuts-issues'));
Mousetrap.bind('shift+m', () => findAndFollowLink('.dashboard-shortcuts-merge_requests'));
Mousetrap.bind('shift+p', () => findAndFollowLink('.dashboard-shortcuts-projects'));
Mousetrap.bind('shift+g', () => findAndFollowLink('.dashboard-shortcuts-groups'));
Mousetrap.bind('shift+l', () => findAndFollowLink('.dashboard-shortcuts-milestones'));
Mousetrap.bind('shift+s', () => findAndFollowLink('.dashboard-shortcuts-snippets'));
Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], this.toggleMarkdownPreview); Mousetrap.bind(['ctrl+shift+p', 'command+shift+p'], this.toggleMarkdownPreview);
if (typeof findFileURL !== "undefined" && findFileURL !== null) { if (typeof findFileURL !== "undefined" && findFileURL !== null) {
Mousetrap.bind('t', function() { Mousetrap.bind('t', function() {
......
/* eslint-disable func-names, space-before-function-paren, max-len, one-var, no-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign */ /**
/* global Mousetrap */ * Helper function that finds the href of the fiven selector and updates the location.
/* global Shortcuts */ *
* @param {String} selector
require('./shortcuts'); */
export default (selector) => {
(function() { const link = document.querySelector(selector).getAttribute('href');
var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty; if (link) {
window.location = link;
this.ShortcutsDashboardNavigation = (function(superClass) { }
extend(ShortcutsDashboardNavigation, superClass); };
function ShortcutsDashboardNavigation() {
ShortcutsDashboardNavigation.__super__.constructor.call(this);
Mousetrap.bind('g a', function() {
return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-activity');
});
Mousetrap.bind('g i', function() {
return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-issues');
});
Mousetrap.bind('g m', function() {
return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-merge_requests');
});
Mousetrap.bind('g t', function() {
return ShortcutsDashboardNavigation.findAndFollowLink('.shortcuts-todos');
});
Mousetrap.bind('g p', function() {
return ShortcutsDashboardNavigation.findAndFollowLink('.dashboard-shortcuts-projects');
});
}
ShortcutsDashboardNavigation.findAndFollowLink = function(selector) {
var link;
link = $(selector).attr('href');
if (link) {
return window.location = link;
}
};
return ShortcutsDashboardNavigation;
})(Shortcuts);
}).call(window);
/* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign */ /* eslint-disable func-names, space-before-function-paren, max-len, no-var, one-var, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, prefer-arrow-callback, consistent-return, no-return-assign */
/* global Mousetrap */ /* global Mousetrap */
/* global Shortcuts */ /* global Shortcuts */
import findAndFollowLink from './shortcuts_dashboard_navigation';
require('./shortcuts'); require('./shortcuts');
...@@ -13,59 +14,23 @@ require('./shortcuts'); ...@@ -13,59 +14,23 @@ require('./shortcuts');
function ShortcutsNavigation() { function ShortcutsNavigation() {
ShortcutsNavigation.__super__.constructor.call(this); ShortcutsNavigation.__super__.constructor.call(this);
Mousetrap.bind('g p', function() { Mousetrap.bind('g p', () => findAndFollowLink('.shortcuts-project'));
return ShortcutsNavigation.findAndFollowLink('.shortcuts-project'); Mousetrap.bind('g e', () => findAndFollowLink('.shortcuts-project-activity'));
}); Mousetrap.bind('g f', () => findAndFollowLink('.shortcuts-tree'));
Mousetrap.bind('g e', function() { Mousetrap.bind('g c', () => findAndFollowLink('.shortcuts-commits'));
return ShortcutsNavigation.findAndFollowLink('.shortcuts-project-activity'); Mousetrap.bind('g j', () => findAndFollowLink('.shortcuts-builds'));
}); Mousetrap.bind('g n', () => findAndFollowLink('.shortcuts-network'));
Mousetrap.bind('g f', function() { Mousetrap.bind('g d', () => findAndFollowLink('.shortcuts-repository-charts'));
return ShortcutsNavigation.findAndFollowLink('.shortcuts-tree'); Mousetrap.bind('g i', () => findAndFollowLink('.shortcuts-issues'));
}); Mousetrap.bind('g b', () => findAndFollowLink('.shortcuts-issue-boards'));
Mousetrap.bind('g c', function() { Mousetrap.bind('g m', () => findAndFollowLink('.shortcuts-merge_requests'));
return ShortcutsNavigation.findAndFollowLink('.shortcuts-commits'); Mousetrap.bind('g t', () => findAndFollowLink('.shortcuts-todos'));
}); Mousetrap.bind('g w', () => findAndFollowLink('.shortcuts-wiki'));
Mousetrap.bind('g b', function() { Mousetrap.bind('g s', () => findAndFollowLink('.shortcuts-snippets'));
return ShortcutsNavigation.findAndFollowLink('.shortcuts-builds'); Mousetrap.bind('i', () => findAndFollowLink('.shortcuts-new-issue'));
});
Mousetrap.bind('g n', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-network');
});
Mousetrap.bind('g g', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-repository-charts');
});
Mousetrap.bind('g i', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-issues');
});
Mousetrap.bind('g l', function() {
ShortcutsNavigation.findAndFollowLink('.shortcuts-issue-boards');
});
Mousetrap.bind('g m', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-merge_requests');
});
Mousetrap.bind('g t', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-todos');
});
Mousetrap.bind('g w', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-wiki');
});
Mousetrap.bind('g s', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-snippets');
});
Mousetrap.bind('i', function() {
return ShortcutsNavigation.findAndFollowLink('.shortcuts-new-issue');
});
this.enabledHelp.push('.hidden-shortcut.project'); this.enabledHelp.push('.hidden-shortcut.project');
} }
ShortcutsNavigation.findAndFollowLink = function(selector) {
var link;
link = $(selector).attr('href');
if (link) {
return window.location = link;
}
};
return ShortcutsNavigation; return ShortcutsNavigation;
})(Shortcuts); })(Shortcuts);
}).call(window); }).call(window);
import Vue from 'vue'; import Vue from 'vue';
import Visibility from 'visibilityjs';
import PipelinesService from './services/pipelines_service'; import PipelinesService from './services/pipelines_service';
import eventHub from './event_hub'; import eventHub from './event_hub';
import PipelinesTableComponent from '../vue_shared/components/pipelines_table'; import PipelinesTableComponent from '../vue_shared/components/pipelines_table';
...@@ -7,6 +8,7 @@ import EmptyState from './components/empty_state'; ...@@ -7,6 +8,7 @@ import EmptyState from './components/empty_state';
import ErrorState from './components/error_state'; import ErrorState from './components/error_state';
import NavigationTabs from './components/navigation_tabs'; import NavigationTabs from './components/navigation_tabs';
import NavigationControls from './components/nav_controls'; import NavigationControls from './components/nav_controls';
import Poll from '../lib/utils/poll';
export default { export default {
props: { props: {
...@@ -47,6 +49,7 @@ export default { ...@@ -47,6 +49,7 @@ export default {
pagenum: 1, pagenum: 1,
isLoading: false, isLoading: false,
hasError: false, hasError: false,
isMakingRequest: false,
}; };
}, },
...@@ -120,18 +123,49 @@ export default { ...@@ -120,18 +123,49 @@ export default {
tagsPath: this.tagsPath, tagsPath: this.tagsPath,
}; };
}, },
pageParameter() {
return gl.utils.getParameterByName('page') || this.pagenum;
},
scopeParameter() {
return gl.utils.getParameterByName('scope') || this.apiScope;
},
}, },
created() { created() {
this.service = new PipelinesService(this.endpoint); this.service = new PipelinesService(this.endpoint);
this.fetchPipelines(); const poll = new Poll({
resource: this.service,
method: 'getPipelines',
data: { page: this.pageParameter, scope: this.scopeParameter },
successCallback: this.successCallback,
errorCallback: this.errorCallback,
notificationCallback: this.setIsMakingRequest,
});
if (!Visibility.hidden()) {
this.isLoading = true;
poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
poll.restart();
} else {
poll.stop();
}
});
eventHub.$on('refreshPipelines', this.fetchPipelines); eventHub.$on('refreshPipelines', this.fetchPipelines);
}, },
beforeUpdate() { beforeUpdate() {
if (this.state.pipelines.length && this.$children) { if (this.state.pipelines.length &&
this.$children &&
!this.isMakingRequest &&
!this.isLoading) {
this.store.startTimeAgoLoops.call(this, Vue); this.store.startTimeAgoLoops.call(this, Vue);
} }
}, },
...@@ -154,27 +188,35 @@ export default { ...@@ -154,27 +188,35 @@ export default {
}, },
fetchPipelines() { fetchPipelines() {
const pageNumber = gl.utils.getParameterByName('page') || this.pagenum; if (!this.isMakingRequest) {
const scope = gl.utils.getParameterByName('scope') || this.apiScope; this.isLoading = true;
this.isLoading = true; this.service.getPipelines({ scope: this.scopeParameter, page: this.pageParameter })
return this.service.getPipelines(scope, pageNumber) .then(response => this.successCallback(response))
.then(resp => ({ .catch(() => this.errorCallback());
headers: resp.headers, }
body: resp.json(), },
}))
.then((response) => { successCallback(resp) {
this.store.storeCount(response.body.count); const response = {
this.store.storePipelines(response.body.pipelines); headers: resp.headers,
this.store.storePagination(response.headers); body: resp.json(),
}) };
.then(() => {
this.isLoading = false; this.store.storeCount(response.body.count);
}) this.store.storePipelines(response.body.pipelines);
.catch(() => { this.store.storePagination(response.headers);
this.hasError = true;
this.isLoading = false; this.isLoading = false;
}); },
errorCallback() {
this.hasError = true;
this.isLoading = false;
},
setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest;
}, },
}, },
......
...@@ -26,7 +26,8 @@ export default class PipelinesService { ...@@ -26,7 +26,8 @@ export default class PipelinesService {
this.pipelines = Vue.resource(endpoint); this.pipelines = Vue.resource(endpoint);
} }
getPipelines(scope, page) { getPipelines(data = {}) {
const { scope, page } = data;
return this.pipelines.get({ scope, page }); return this.pipelines.get({ scope, page });
} }
......
...@@ -91,7 +91,7 @@ ...@@ -91,7 +91,7 @@
.award-menu-holder { .award-menu-holder {
display: inline-block; display: inline-block;
position: relative; position: absolute;
.tooltip { .tooltip {
white-space: nowrap; white-space: nowrap;
...@@ -117,11 +117,41 @@ ...@@ -117,11 +117,41 @@
&.active, &.active,
&:hover, &:hover,
&:active { &:active,
&.is-active {
background-color: $row-hover; background-color: $row-hover;
border-color: $row-hover-border; border-color: $row-hover-border;
box-shadow: none; box-shadow: none;
outline: 0; outline: 0;
.award-control-icon svg {
background: $award-emoji-positive-add-bg;
path {
fill: $award-emoji-positive-add-lines;
}
}
.award-control-icon-neutral {
opacity: 0;
}
.award-control-icon-positive {
opacity: 1;
transform: scale(1.15);
}
}
&.is-active {
.award-control-icon-positive {
opacity: 0;
transform: scale(1);
}
.award-control-icon-super-positive {
opacity: 1;
transform: scale(1);
}
} }
&.btn { &.btn {
...@@ -162,9 +192,33 @@ ...@@ -162,9 +192,33 @@
color: $border-gray-normal; color: $border-gray-normal;
margin-top: 1px; margin-top: 1px;
padding: 0 2px; padding: 0 2px;
svg {
margin-bottom: 1px;
height: 18px;
width: 18px;
border-radius: 50%;
path {
fill: $border-gray-normal;
}
}
}
.award-control-icon-positive,
.award-control-icon-super-positive {
position: absolute;
left: 7px;
bottom: 9px;
opacity: 0;
@include transition(opacity, transform);
} }
.award-control-text { .award-control-text {
vertical-align: middle; vertical-align: middle;
} }
} }
.note-awards .award-control-icon-positive {
left: 6px;
}
...@@ -187,6 +187,15 @@ ...@@ -187,6 +187,15 @@
} }
} }
.shortcut-mappings {
display: none;
}
&.shortcuts .shortcut-mappings {
display: inline-block;
margin-right: 5px;
}
ul { ul {
margin: 0; margin: 0;
padding: 0; padding: 0;
......
...@@ -446,10 +446,8 @@ ...@@ -446,10 +446,8 @@
} }
} }
.filter-dropdown-item.dropdown-active { .filter-dropdown-item.droplab-item-active .btn {
.btn { @extend %filter-dropdown-item-btn-hover;
@extend %filter-dropdown-item-btn-hover;
}
} }
.filter-dropdown-loading { .filter-dropdown-loading {
......
...@@ -16,6 +16,8 @@ body.modal-open { ...@@ -16,6 +16,8 @@ body.modal-open {
overflow: hidden; overflow: hidden;
} }
.modal .modal-dialog { @media (min-width: $screen-md-min) {
width: 860px; .modal-dialog {
width: 860px;
}
} }
.timeline { .timeline {
@include basic-list; @include basic-list;
margin: 0; margin: 0;
padding: 0; padding: 0;
.timeline-entry { .timeline-entry {
padding: $gl-padding $gl-btn-padding 11px; padding: $gl-padding $gl-btn-padding 14px;
border-color: $white-normal; border-color: $white-normal;
color: $gl-text-color; color: $gl-text-color;
border-bottom: 1px solid $border-white-light; border-bottom: 1px solid $border-white-light;
.timeline-entry-inner {
position: relative;
}
&:target { &:target {
background: $line-target-blue; background: $line-target-blue;
} }
......
...@@ -296,6 +296,8 @@ $badge-color: $gl-text-color-secondary; ...@@ -296,6 +296,8 @@ $badge-color: $gl-text-color-secondary;
* Award emoji * Award emoji
*/ */
$award-emoji-menu-shadow: rgba(0,0,0,.175); $award-emoji-menu-shadow: rgba(0,0,0,.175);
$award-emoji-positive-add-bg: #fed159;
$award-emoji-positive-add-lines: #bb9c13;
/* /*
* Search Box * Search Box
......
...@@ -226,7 +226,7 @@ ...@@ -226,7 +226,7 @@
.card { .card {
position: relative; position: relative;
padding: 10px $gl-padding; padding: 11px 10px 11px $gl-padding;
background: $white-light; background: $white-light;
border-radius: $border-radius-default; border-radius: $border-radius-default;
box-shadow: 0 1px 2px $issue-boards-card-shadow; box-shadow: 0 1px 2px $issue-boards-card-shadow;
...@@ -246,6 +246,8 @@ ...@@ -246,6 +246,8 @@
} }
.confidential-icon { .confidential-icon {
position: relative;
top: 1px;
margin-right: 5px; margin-right: 5px;
} }
} }
...@@ -253,34 +255,43 @@ ...@@ -253,34 +255,43 @@
.card-title { .card-title {
margin: 0; margin: 0;
font-size: 1em; font-size: 1em;
line-height: inherit;
a { a {
color: inherit; color: $gl-text-color;
word-wrap: break-word; word-wrap: break-word;
margin-right: 2px;
} }
} }
.card-footer { .card-header {
margin-top: 5px; display: flex;
line-height: 25px; min-height: 20px;
.label {
margin-right: 5px;
font-size: (14px / $issue-boards-font-size) * 1em;
}
.card-assignee { .card-assignee {
margin-left: auto;
margin-right: 5px; margin-right: 5px;
padding-left: 10px;
height: 20px;
} }
.avatar { .avatar {
margin-left: 0; margin: 0;
margin-right: 0; }
}
.card-footer {
margin: 0 0 5px;
.label {
margin-top: 5px;
margin-right: 6px;
} }
} }
.card-number { .card-number {
margin-right: 5px; font-size: 12px;
color: $gl-text-color-secondary;
} }
.issue-boards-search { .issue-boards-search {
......
...@@ -57,6 +57,37 @@ ...@@ -57,6 +57,37 @@
margin-right: 5px; margin-right: 5px;
} }
} }
.truncated-info {
text-align: center;
border-bottom: 1px solid;
background-color: $black-transparent;
height: 45px;
&.affix {
top: 0;
}
// with sidebar
&.affix.sidebar-expanded {
right: 312px;
left: 22px;
}
// without sidebar
&.affix.sidebar-collapsed {
right: 20px;
left: 20px;
}
&.affix-top {
position: absolute;
top: 0;
margin: 0 auto;
right: 5px;
left: 5px;
}
}
} }
.scroll-controls { .scroll-controls {
...@@ -186,6 +217,7 @@ ...@@ -186,6 +217,7 @@
white-space: pre; white-space: pre;
overflow-x: auto; overflow-x: auto;
font-size: 12px; font-size: 12px;
position: relative;
.fa-refresh { .fa-refresh {
font-size: 24px; font-size: 24px;
......
...@@ -383,6 +383,15 @@ ...@@ -383,6 +383,15 @@
stroke-width: 1; stroke-width: 1;
} }
.prometheus-state {
margin-top: 10px;
display: none;
.state-button-section {
margin-top: 10px;
}
}
.environments-actions { .environments-actions {
.external-url, .external-url,
.monitoring-url, .monitoring-url,
......
...@@ -4,14 +4,14 @@ ...@@ -4,14 +4,14 @@
*/ */
.event-item { .event-item {
font-size: $gl-font-size; font-size: $gl-font-size;
padding: $gl-padding-top 0 $gl-padding-top ($gl-avatar-size + $gl-padding-top); padding: $gl-padding-top 0 $gl-padding-top 40px;
border-bottom: 1px solid $white-normal; border-bottom: 1px solid $white-normal;
color: $list-text-color; color: $list-text-color;
position: relative;
&.event-inline { &.event-inline {
.avatar { .profile-icon {
position: relative; top: 20px;
top: -2px;
} }
.event-title, .event-title,
...@@ -24,8 +24,28 @@ ...@@ -24,8 +24,28 @@
color: $gl-text-color; color: $gl-text-color;
} }
.avatar { .profile-icon {
margin-left: -($gl-avatar-size + $gl-padding-top); position: absolute;
left: 0;
top: 14px;
svg {
width: 20px;
height: auto;
fill: $gl-text-color-secondary;
}
&.open-icon svg {
fill: $green-300;
}
&.closed-icon svg {
fill: $red-300;
}
&.fork-icon svg {
fill: $blue-300;
}
} }
.event-title { .event-title {
...@@ -163,7 +183,7 @@ ...@@ -163,7 +183,7 @@
max-width: 100%; max-width: 100%;
} }
.avatar { .profile-icon {
display: none; display: none;
} }
......
...@@ -196,6 +196,7 @@ ...@@ -196,6 +196,7 @@
transition: width .3s; transition: width .3s;
background: $gray-light; background: $gray-light;
padding: 10px 20px; padding: 10px 20px;
z-index: 2;
&.right-sidebar-expanded { &.right-sidebar-expanded {
width: $gutter_width; width: $gutter_width;
......
...@@ -329,8 +329,6 @@ ...@@ -329,8 +329,6 @@
} }
#modal_merge_info .modal-dialog { #modal_merge_info .modal-dialog {
width: 600px;
.dark { .dark {
margin-right: 40px; margin-right: 40px;
} }
......
...@@ -16,6 +16,15 @@ ul.notes { ...@@ -16,6 +16,15 @@ ul.notes {
.timeline-icon { .timeline-icon {
float: left; float: left;
svg {
width: 18px;
height: auto;
fill: $gray-darkest;
position: absolute;
left: 30px;
top: 15px;
}
} }
.timeline-content { .timeline-content {
...@@ -33,6 +42,103 @@ ul.notes { ...@@ -33,6 +42,103 @@ ul.notes {
white-space: nowrap; white-space: nowrap;
} }
.discussion-body {
padding-top: 15px;
}
.discussion {
overflow: hidden;
display: block;
position: relative;
}
.note {
display: block;
position: relative;
border-bottom: 1px solid $white-normal;
&.note-discussion {
&.timeline-entry {
padding: 14px 10px;
}
.system-note {
padding: 0;
}
}
&.is-editting {
.note-header,
.note-text,
.edited-text {
display: none;
}
.note-edit-form {
display: block;
&.current-note-edit-form + .note-awards {
display: none;
}
}
}
.note-body {
overflow-x: auto;
overflow-y: hidden;
.note-text {
word-wrap: break-word;
@include md-typography;
// Reset ul style types since we're nested inside a ul already
@include bulleted-list;
ul.task-list {
ul:not(.task-list) {
padding-left: 1.3em;
}
}
}
}
.note-awards {
.js-awards-block {
padding: 2px;
margin-top: 10px;
}
}
.note-header {
padding-bottom: 3px;
padding-right: 20px;
@media (min-width: $screen-sm-min) {
padding-right: 0;
}
@media (max-width: $screen-xs-min) {
.inline {
display: block;
}
}
}
.note-emoji-button {
.fa-spinner {
display: none;
}
&.is-loading {
.fa-smile-o {
display: none;
}
.fa-spinner {
display: inline-block;
}
}
}
}
.system-note { .system-note {
font-size: 14px; font-size: 14px;
padding: 0; padding: 0;
...@@ -68,6 +174,10 @@ ul.notes { ...@@ -68,6 +174,10 @@ ul.notes {
padding: 14px 10px; padding: 14px 10px;
} }
.note-header {
padding-bottom: 0;
}
.note-body { .note-body {
overflow: hidden; overflow: hidden;
...@@ -130,116 +240,6 @@ ul.notes { ...@@ -130,116 +240,6 @@ ul.notes {
} }
} }
} }
.timeline-icon {
display: none;
.avatar {
visibility: hidden;
.discussion-body & {
visibility: visible;
}
}
}
}
.discussion-body {
padding-top: 15px;
}
.discussion {
overflow: hidden;
display: block;
position: relative;
}
.note {
display: block;
position: relative;
border-bottom: 1px solid $white-normal;
&.note-discussion {
&.timeline-entry {
padding: 14px 10px;
}
.system-note {
padding: 0;
}
}
&.is-editting {
.note-header,
.note-text,
.edited-text {
display: none;
}
.note-edit-form {
display: block;
&.current-note-edit-form + .note-awards {
display: none;
}
}
}
.note-body {
overflow-x: auto;
overflow-y: hidden;
.note-text {
word-wrap: break-word;
@include md-typography;
// Reset ul style types since we're nested inside a ul already
@include bulleted-list;
ul.task-list {
ul:not(.task-list) {
padding-left: 1.3em;
}
}
}
}
.note-awards {
.js-awards-block {
padding: 2px;
margin-top: 10px;
}
}
.note-header {
padding-bottom: 3px;
padding-right: 20px;
@media (min-width: $screen-sm-min) {
padding-right: 0;
}
@media (max-width: $screen-xs-min) {
.inline {
display: block;
}
}
}
.note-emoji-button {
.fa-spinner {
display: none;
}
&.is-loading {
.fa-smile-o {
display: none;
}
.fa-spinner {
display: inline-block;
}
}
}
} }
} }
...@@ -398,13 +398,50 @@ ul.notes { ...@@ -398,13 +398,50 @@ ul.notes {
font-size: 17px; font-size: 17px;
} }
&:hover { svg {
height: 16px;
width: 16px;
fill: $gray-darkest;
vertical-align: text-top;
}
.award-control-icon-positive,
.award-control-icon-super-positive {
position: absolute;
margin-left: -20px;
opacity: 0;
}
&:hover,
&.is-active {
.danger-highlight { .danger-highlight {
color: $gl-text-red; color: $gl-text-red;
} }
.link-highlight { .link-highlight {
color: $gl-link-color; color: $gl-link-color;
svg {
fill: $gl-link-color;
}
}
.award-control-icon-neutral {
opacity: 0;
}
.award-control-icon-positive {
opacity: 1;
}
}
&.is-active {
.award-control-icon-positive {
opacity: 0;
}
.award-control-icon-super-positive {
opacity: 1;
} }
} }
} }
...@@ -508,7 +545,6 @@ ul.notes { ...@@ -508,7 +545,6 @@ ul.notes {
} }
.line-resolve-all-container { .line-resolve-all-container {
.btn-group { .btn-group {
margin-left: -4px; margin-left: -4px;
} }
...@@ -537,7 +573,6 @@ ul.notes { ...@@ -537,7 +573,6 @@ ul.notes {
fill: $gray-darkest; fill: $gray-darkest;
} }
} }
} }
.line-resolve-all { .line-resolve-all {
......
...@@ -230,6 +230,14 @@ ...@@ -230,6 +230,14 @@
font-size: 0; font-size: 0;
} }
.fade-right {
right: 0;
}
.fade-left {
left: 0;
}
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
.cover-block { .cover-block {
padding-top: 20px; padding-top: 20px;
......
...@@ -790,10 +790,13 @@ a.allowed-to-push { ...@@ -790,10 +790,13 @@ a.allowed-to-push {
} }
} }
<<<<<<< HEAD
.disabled-item { .disabled-item {
@extend .btn.disabled; @extend .btn.disabled;
} }
=======
>>>>>>> ce/master
.protected-tags-list { .protected-tags-list {
.dropdown-menu-toggle { .dropdown-menu-toggle {
width: 100%; width: 100%;
......
...@@ -6,6 +6,8 @@ ...@@ -6,6 +6,8 @@
} }
.trigger-actions { .trigger-actions {
white-space: nowrap;
.btn { .btn {
margin-left: 10px; margin-left: 10px;
} }
......
...@@ -145,8 +145,6 @@ ...@@ -145,8 +145,6 @@
margin: 0; margin: 0;
} }
#modal-remove-blob > .modal-dialog { width: 850px; }
.blob-upload-dropzone-previews { .blob-upload-dropzone-previews {
text-align: center; text-align: center;
border: 2px; border: 2px;
......
...@@ -72,7 +72,9 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -72,7 +72,9 @@ class Admin::GroupsController < Admin::ApplicationController
:name, :name,
:path, :path,
:request_access_enabled, :request_access_enabled,
:visibility_level :visibility_level,
:require_two_factor_authentication,
:two_factor_grace_period
] ]
end end
......
...@@ -8,12 +8,12 @@ class ApplicationController < ActionController::Base ...@@ -8,12 +8,12 @@ class ApplicationController < ActionController::Base
include PageLayoutHelper include PageLayoutHelper
include SentryHelper include SentryHelper
include WorkhorseHelper include WorkhorseHelper
include EnforcesTwoFactorAuthentication
before_action :authenticate_user_from_private_token! before_action :authenticate_user_from_private_token!
before_action :authenticate_user! before_action :authenticate_user!
before_action :validate_user_service_ticket! before_action :validate_user_service_ticket!
before_action :check_password_expiration before_action :check_password_expiration
before_action :check_2fa_requirement
before_action :ldap_security_check before_action :ldap_security_check
before_action :sentry_context before_action :sentry_context
before_action :default_headers before_action :default_headers
...@@ -155,12 +155,6 @@ class ApplicationController < ActionController::Base ...@@ -155,12 +155,6 @@ class ApplicationController < ActionController::Base
end end
end end
def check_2fa_requirement
if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled? && !skip_two_factor?
redirect_to profile_two_factor_auth_path
end
end
def ldap_security_check def ldap_security_check
if current_user && current_user.requires_ldap_check? if current_user && current_user.requires_ldap_check?
return unless current_user.try_obtain_ldap_lease return unless current_user.try_obtain_ldap_lease
...@@ -269,23 +263,6 @@ class ApplicationController < ActionController::Base ...@@ -269,23 +263,6 @@ class ApplicationController < ActionController::Base
current_application_settings.import_sources.include?('gitlab_project') current_application_settings.import_sources.include?('gitlab_project')
end end
def two_factor_authentication_required?
current_application_settings.require_two_factor_authentication
end
def two_factor_grace_period
current_application_settings.two_factor_grace_period
end
def two_factor_grace_period_expired?
date = current_user.otp_grace_period_started_at
date && (date + two_factor_grace_period.hours) < Time.current
end
def skip_two_factor?
session[:skip_tfa] && session[:skip_tfa] > Time.current
end
# U2F (universal 2nd factor) devices need a unique identifier for the application # U2F (universal 2nd factor) devices need a unique identifier for the application
# to perform authentication. # to perform authentication.
# https://developers.yubico.com/U2F/App_ID.html # https://developers.yubico.com/U2F/App_ID.html
......
# == EnforcesTwoFactorAuthentication
#
# Controller concern to enforce two-factor authentication requirements
#
# Upon inclusion, adds `check_two_factor_requirement` as a before_action,
# and makes `two_factor_grace_period_expired?` and `two_factor_skippable?`
# available as view helpers.
module EnforcesTwoFactorAuthentication
extend ActiveSupport::Concern
included do
before_action :check_two_factor_requirement
helper_method :two_factor_grace_period_expired?, :two_factor_skippable?
end
def check_two_factor_requirement
if two_factor_authentication_required? && current_user && !current_user.two_factor_enabled? && !skip_two_factor?
redirect_to profile_two_factor_auth_path
end
end
def two_factor_authentication_required?
current_application_settings.require_two_factor_authentication? ||
current_user.try(:require_two_factor_authentication_from_group?)
end
def two_factor_authentication_reason(global: -> {}, group: -> {})
if two_factor_authentication_required?
if current_application_settings.require_two_factor_authentication?
global.call
else
groups = current_user.expanded_groups_requiring_two_factor_authentication.reorder(name: :asc)
group.call(groups)
end
end
end
def two_factor_grace_period
periods = [current_application_settings.two_factor_grace_period]
periods << current_user.two_factor_grace_period if current_user.try(:require_two_factor_authentication_from_group?)
periods.min
end
def two_factor_grace_period_expired?
date = current_user.otp_grace_period_started_at
date && (date + two_factor_grace_period.hours) < Time.current
end
def two_factor_skippable?
two_factor_authentication_required? &&
!current_user.two_factor_enabled? &&
!two_factor_grace_period_expired?
end
def skip_two_factor?
session[:skip_two_factor] && session[:skip_two_factor] > Time.current
end
end
module RequiresHealthToken
extend ActiveSupport::Concern
included do
before_action :validate_health_check_access!
end
private
def validate_health_check_access!
render_404 unless token_valid?
end
def token_valid?
token = params[:token].presence || request.headers['TOKEN']
token.present? &&
ActiveSupport::SecurityUtils.variable_size_secure_compare(
token,
current_application_settings.health_check_access_token
)
end
def render_404
render file: Rails.root.join('public', '404'), layout: false, status: '404'
end
end
...@@ -151,7 +151,9 @@ class GroupsController < Groups::ApplicationController ...@@ -151,7 +151,9 @@ class GroupsController < Groups::ApplicationController
:visibility_level, :visibility_level,
:parent_id, :parent_id,
:create_chat_team, :create_chat_team,
:chat_team_name :chat_team_name,
:require_two_factor_authentication,
:two_factor_grace_period
] ]
end end
......
class HealthCheckController < HealthCheck::HealthCheckController class HealthCheckController < HealthCheck::HealthCheckController
before_action :validate_health_check_access! include RequiresHealthToken
private
def validate_health_check_access!
render_404 unless token_valid?
end
def token_valid?
token = params[:token].presence || request.headers['TOKEN']
token.present? &&
ActiveSupport::SecurityUtils.variable_size_secure_compare(
token,
current_application_settings.health_check_access_token
)
end
def render_404
render file: Rails.root.join('public', '404'), layout: false, status: '404'
end
end end
class HealthController < ActionController::Base
protect_from_forgery with: :exception
include RequiresHealthToken
CHECKS = [
Gitlab::HealthChecks::DbCheck,
Gitlab::HealthChecks::RedisCheck,
Gitlab::HealthChecks::FsShardsCheck,
].freeze
def readiness
results = CHECKS.map { |check| [check.name, check.readiness] }
render_check_results(results)
end
def liveness
results = CHECKS.map { |check| [check.name, check.liveness] }
render_check_results(results)
end
def metrics
results = CHECKS.flat_map(&:metrics)
response = results.map(&method(:metric_to_prom_line)).join("\n")
render text: response, content_type: 'text/plain; version=0.0.4'
end
private
def metric_to_prom_line(metric)
labels = metric.labels&.map { |key, value| "#{key}=\"#{value}\"" }&.join(',') || ''
if labels.empty?
"#{metric.name} #{metric.value}"
else
"#{metric.name}{#{labels}} #{metric.value}"
end
end
def render_check_results(results)
flattened = results.flat_map do |name, result|
if result.is_a?(Gitlab::HealthChecks::Result)
[[name, result]]
else
result.map { |r| [name, r] }
end
end
success = flattened.all? { |name, r| r.success }
response = flattened.map do |name, r|
info = { status: r.success ? 'ok' : 'failed' }
info['message'] = r.message if r.message
info[:labels] = r.labels if r.labels
[name, info]
end
render json: response.to_h, status: success ? :ok : :service_unavailable
end
end
class Profiles::TwoFactorAuthsController < Profiles::ApplicationController class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
skip_before_action :check_2fa_requirement skip_before_action :check_two_factor_requirement
def show def show
unless current_user.otp_secret unless current_user.otp_secret
...@@ -13,11 +13,24 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -13,11 +13,24 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
current_user.save! if current_user.changed? current_user.save! if current_user.changed?
if two_factor_authentication_required? && !current_user.two_factor_enabled? if two_factor_authentication_required? && !current_user.two_factor_enabled?
if two_factor_grace_period_expired? two_factor_authentication_reason(
flash.now[:alert] = 'You must enable Two-Factor Authentication for your account.' global: lambda do
else flash.now[:alert] =
'The global settings require you to enable Two-Factor Authentication for your account.'
end,
group: lambda do |groups|
group_links = groups.map { |group| view_context.link_to group.full_name, group_path(group) }.to_sentence
flash.now[:alert] = %{
The group settings for #{group_links} require you to enable
Two-Factor Authentication for your account.
}.html_safe
end
)
unless two_factor_grace_period_expired?
grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours grace_period_deadline = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
flash.now[:alert] = "You must enable Two-Factor Authentication for your account before #{l(grace_period_deadline)}." flash.now[:alert] << " You need to do this before #{l(grace_period_deadline)}."
end end
end end
...@@ -71,7 +84,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController ...@@ -71,7 +84,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
if two_factor_grace_period_expired? if two_factor_grace_period_expired?
redirect_to new_profile_two_factor_auth_path, alert: 'Cannot skip two factor authentication setup' redirect_to new_profile_two_factor_auth_path, alert: 'Cannot skip two factor authentication setup'
else else
session[:skip_tfa] = current_user.otp_grace_period_started_at + two_factor_grace_period.hours session[:skip_two_factor] = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
redirect_to root_path redirect_to root_path
end end
end end
......
...@@ -31,25 +31,25 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -31,25 +31,25 @@ class Projects::BuildsController < Projects::ApplicationController
@builds = @project.pipelines.find_by_sha(@build.sha).builds.order('id DESC') @builds = @project.pipelines.find_by_sha(@build.sha).builds.order('id DESC')
@builds = @builds.where("id not in (?)", @build.id) @builds = @builds.where("id not in (?)", @build.id)
@pipeline = @build.pipeline @pipeline = @build.pipeline
respond_to do |format|
format.html
format.json do
render json: {
id: @build.id,
status: @build.status,
trace_html: @build.trace_html
}
end
end
end end
def trace def trace
respond_to do |format| build.trace.read do |stream|
format.json do respond_to do |format|
state = params[:state].presence format.json do
render json: @build.trace_with_state(state: state). result = {
merge!(id: @build.id, status: @build.status) id: @build.id, status: @build.status, complete: @build.complete?
}
if stream.valid?
stream.limit
state = params[:state].presence
trace = stream.html_with_state(state)
result.merge!(trace.to_h)
end
render json: result
end
end end
end end
end end
...@@ -86,10 +86,12 @@ class Projects::BuildsController < Projects::ApplicationController ...@@ -86,10 +86,12 @@ class Projects::BuildsController < Projects::ApplicationController
end end
def raw def raw
if @build.has_trace_file? build.trace.read do |stream|
send_file @build.trace_file_path, type: 'text/plain; charset=utf-8', disposition: 'inline' if stream.file?
else send_file stream.path, type: 'text/plain; charset=utf-8', disposition: 'inline'
render_404 else
render_404
end
end end
end end
......
...@@ -35,6 +35,8 @@ class Projects::CommitController < Projects::ApplicationController ...@@ -35,6 +35,8 @@ class Projects::CommitController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
Gitlab::PollingInterval.set_header(response, interval: 10_000)
render json: PipelineSerializer render json: PipelineSerializer
.new(project: @project, user: @current_user) .new(project: @project, user: @current_user)
.represent(@pipelines) .represent(@pipelines)
......
...@@ -236,6 +236,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -236,6 +236,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
format.json do format.json do
Gitlab::PollingInterval.set_header(response, interval: 10_000)
render json: PipelineSerializer render json: PipelineSerializer
.new(project: @project, user: @current_user) .new(project: @project, user: @current_user)
.represent(@pipelines) .represent(@pipelines)
...@@ -249,6 +251,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -249,6 +251,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
format.json do format.json do
define_pipelines_vars define_pipelines_vars
Gitlab::PollingInterval.set_header(response, interval: 10_000)
render json: { render json: {
pipelines: PipelineSerializer pipelines: PipelineSerializer
.new(project: @project, user: @current_user) .new(project: @project, user: @current_user)
...@@ -478,7 +482,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -478,7 +482,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
if pipeline if pipeline
status = pipeline.status status = pipeline.status
coverage = pipeline.try(:coverage) coverage = pipeline.coverage
status = "success_with_warnings" if pipeline.success? && pipeline.has_warnings? status = "success_with_warnings" if pipeline.success? && pipeline.has_warnings?
......
...@@ -29,6 +29,8 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -29,6 +29,8 @@ class Projects::PipelinesController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
Gitlab::PollingInterval.set_header(response, interval: 10_000)
render json: { render json: {
pipelines: PipelineSerializer pipelines: PipelineSerializer
.new(project: @project, user: @current_user) .new(project: @project, user: @current_user)
...@@ -114,7 +116,7 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -114,7 +116,7 @@ class Projects::PipelinesController < Projects::ApplicationController
end end
def pipeline def pipeline
@pipeline ||= project.pipelines.find_by!(id: params[:id]) @pipeline ||= project.pipelines.find_by!(id: params[:id]).present(current_user: current_user)
end end
def commit def commit
......
...@@ -23,7 +23,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController ...@@ -23,7 +23,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
def update_params def update_params
params.require(:project).permit( params.require(:project).permit(
:runners_token, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex, :runners_token, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
:public_builds :public_builds, :auto_cancel_pending_pipelines
) )
end end
end end
...@@ -3,6 +3,7 @@ class Projects::ProtectedBranchesController < Projects::ProtectedRefsController ...@@ -3,6 +3,7 @@ class Projects::ProtectedBranchesController < Projects::ProtectedRefsController
def project_refs def project_refs
@project.repository.branches @project.repository.branches
<<<<<<< HEAD
end end
def create_service_class def create_service_class
...@@ -19,6 +20,20 @@ class Projects::ProtectedBranchesController < Projects::ProtectedRefsController ...@@ -19,6 +20,20 @@ class Projects::ProtectedBranchesController < Projects::ProtectedRefsController
def access_levels def access_levels
[:merge_access_levels, :push_access_levels] [:merge_access_levels, :push_access_levels]
=======
end
def create_service_class
::ProtectedBranches::CreateService
end
def update_service_class
::ProtectedBranches::UpdateService
end
def load_protected_ref
@protected_ref = @project.protected_branches.find(params[:id])
>>>>>>> ce/master
end end
def protected_ref_params def protected_ref_params
......
...@@ -30,7 +30,11 @@ class Projects::ProtectedRefsController < Projects::ApplicationController ...@@ -30,7 +30,11 @@ class Projects::ProtectedRefsController < Projects::ApplicationController
@protected_ref = update_service_class.new(@project, current_user, protected_ref_params).execute(@protected_ref) @protected_ref = update_service_class.new(@project, current_user, protected_ref_params).execute(@protected_ref)
if @protected_ref.valid? if @protected_ref.valid?
<<<<<<< HEAD
render json: @protected_ref, status: :ok, include: access_levels render json: @protected_ref, status: :ok, include: access_levels
=======
render json: @protected_ref, status: :ok
>>>>>>> ce/master
else else
render json: @protected_ref.errors, status: :unprocessable_entity render json: @protected_ref.errors, status: :unprocessable_entity
end end
......
...@@ -17,10 +17,13 @@ class Projects::ProtectedTagsController < Projects::ProtectedRefsController ...@@ -17,10 +17,13 @@ class Projects::ProtectedTagsController < Projects::ProtectedRefsController
@protected_ref = @project.protected_tags.find(params[:id]) @protected_ref = @project.protected_tags.find(params[:id])
end end
<<<<<<< HEAD
def access_levels def access_levels
[:create_access_levels] [:create_access_levels]
end end
=======
>>>>>>> ce/master
def protected_ref_params def protected_ref_params
params.require(:protected_tag).permit(:name, create_access_levels_attributes: [:access_level, :id]) params.require(:protected_tag).permit(:name, create_access_levels_attributes: [:access_level, :id])
end end
......
...@@ -8,13 +8,17 @@ module Projects ...@@ -8,13 +8,17 @@ module Projects
@deploy_keys = DeployKeysPresenter.new(@project, current_user: current_user) @deploy_keys = DeployKeysPresenter.new(@project, current_user: current_user)
define_protected_refs define_protected_refs
<<<<<<< HEAD
project.create_push_rule unless project.push_rule project.create_push_rule unless project.push_rule
@push_rule = project.push_rule @push_rule = project.push_rule
=======
>>>>>>> ce/master
end end
private private
<<<<<<< HEAD
def remote_mirror def remote_mirror
@remote_mirror = @project.remote_mirrors.first_or_initialize @remote_mirror = @project.remote_mirrors.first_or_initialize
end end
...@@ -31,6 +35,18 @@ module Projects ...@@ -31,6 +35,18 @@ module Projects
{ {
selected_merge_access_levels: @protected_branch.merge_access_levels.map { |access_level| access_level.user_id || access_level.access_level }, selected_merge_access_levels: @protected_branch.merge_access_levels.map { |access_level| access_level.user_id || access_level.access_level },
selected_push_access_levels: @protected_branch.push_access_levels.map { |access_level| access_level.user_id || access_level.access_level }, selected_push_access_levels: @protected_branch.push_access_levels.map { |access_level| access_level.user_id || access_level.access_level },
=======
def define_protected_refs
@protected_branches = @project.protected_branches.order(:name).page(params[:page])
@protected_tags = @project.protected_tags.order(:name).page(params[:page])
@protected_branch = @project.protected_branches.new
@protected_tag = @project.protected_tags.new
load_gon_index
end
def access_levels_options
{
>>>>>>> ce/master
create_access_levels: levels_for_dropdown(ProtectedTag::CreateAccessLevel), create_access_levels: levels_for_dropdown(ProtectedTag::CreateAccessLevel),
push_access_levels: levels_for_dropdown(ProtectedBranch::PushAccessLevel), push_access_levels: levels_for_dropdown(ProtectedBranch::PushAccessLevel),
merge_access_levels: levels_for_dropdown(ProtectedBranch::MergeAccessLevel) merge_access_levels: levels_for_dropdown(ProtectedBranch::MergeAccessLevel)
...@@ -56,7 +72,10 @@ module Projects ...@@ -56,7 +72,10 @@ module Projects
gon.push(protectable_tags_for_dropdown) gon.push(protectable_tags_for_dropdown)
gon.push(protectable_branches_for_dropdown) gon.push(protectable_branches_for_dropdown)
gon.push(access_levels_options) gon.push(access_levels_options)
<<<<<<< HEAD
gon.push(current_project_id: @project.id) if @project gon.push(current_project_id: @project.id) if @project
=======
>>>>>>> ce/master
end end
end end
end end
......
...@@ -3,7 +3,7 @@ class SessionsController < Devise::SessionsController ...@@ -3,7 +3,7 @@ class SessionsController < Devise::SessionsController
include Devise::Controllers::Rememberable include Devise::Controllers::Rememberable
include Recaptcha::ClientHelper include Recaptcha::ClientHelper
skip_before_action :check_2fa_requirement, only: [:destroy] skip_before_action :check_two_factor_requirement, only: [:destroy]
prepend_before_action :check_initial_setup, only: [:new] prepend_before_action :check_initial_setup, only: [:new]
prepend_before_action :authenticate_with_two_factor, prepend_before_action :authenticate_with_two_factor,
......
...@@ -68,18 +68,6 @@ module AuthHelper ...@@ -68,18 +68,6 @@ module AuthHelper
current_user.identities.exists?(provider: provider.to_s) current_user.identities.exists?(provider: provider.to_s)
end end
def two_factor_skippable?
current_application_settings.require_two_factor_authentication &&
!current_user.two_factor_enabled? &&
current_application_settings.two_factor_grace_period &&
!two_factor_grace_period_expired?
end
def two_factor_grace_period_expired?
current_user.otp_grace_period_started_at &&
(current_user.otp_grace_period_started_at + current_application_settings.two_factor_grace_period.hours) < Time.current
end
def unlink_allowed?(provider) def unlink_allowed?(provider)
%w(saml cas3).exclude?(provider.to_s) %w(saml cas3).exclude?(provider.to_s)
end end
......
...@@ -102,7 +102,7 @@ module BlobHelper ...@@ -102,7 +102,7 @@ module BlobHelper
if Gitlab::MarkupHelper.previewable?(filename) if Gitlab::MarkupHelper.previewable?(filename)
'Preview' 'Preview'
else else
'Preview Changes' 'Preview changes'
end end
end end
...@@ -210,13 +210,13 @@ module BlobHelper ...@@ -210,13 +210,13 @@ module BlobHelper
end end
def copy_file_path_button(file_path) def copy_file_path_button(file_path)
clipboard_button(clipboard_text: file_path, class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard') clipboard_button(text: file_path, gfm: "`#{file_path}`", class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy file path to clipboard')
end end
def copy_blob_content_button(blob) def copy_blob_content_button(blob)
return if markup?(blob.name) return if markup?(blob.name)
clipboard_button(clipboard_target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm", title: "Copy content to clipboard") clipboard_button(target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm", title: "Copy content to clipboard")
end end
def open_raw_file_button(path) def open_raw_file_button(path)
......
...@@ -33,6 +33,7 @@ module BranchesHelper ...@@ -33,6 +33,7 @@ module BranchesHelper
def protected_branch?(project, branch) def protected_branch?(project, branch)
ProtectedBranch.protected?(project, branch.name) ProtectedBranch.protected?(project, branch.name)
end end
<<<<<<< HEAD
def access_levels_data(access_levels) def access_levels_data(access_levels)
access_levels.map do |level| access_levels.map do |level|
...@@ -52,4 +53,6 @@ module BranchesHelper ...@@ -52,4 +53,6 @@ module BranchesHelper
end end
end end
end end
=======
>>>>>>> ce/master
end end
module ButtonHelper module ButtonHelper
# Output a "Copy to Clipboard" button # Output a "Copy to Clipboard" button
# #
# data - Data attributes passed to `content_tag` # data - Data attributes passed to `content_tag` (default: {}):
# :text - Text to copy (optional)
# :gfm - GitLab Flavored Markdown to copy, if different from `text` (optional)
# :target - Selector for target element to copy from (optional)
# #
# Examples: # Examples:
# #
# # Define the clipboard's text # # Define the clipboard's text
# clipboard_button(clipboard_text: "Foo") # clipboard_button(text: "Foo")
# # => "<button class='...' data-clipboard-text='Foo'>...</button>" # # => "<button class='...' data-clipboard-text='Foo'>...</button>"
# #
# # Define the target element # # Define the target element
# clipboard_button(clipboard_target: "div#foo") # clipboard_button(target: "div#foo")
# # => "<button class='...' data-clipboard-target='div#foo'>...</button>" # # => "<button class='...' data-clipboard-target='div#foo'>...</button>"
# #
# See http://clipboardjs.com/#usage # See http://clipboardjs.com/#usage
def clipboard_button(data = {}) def clipboard_button(data = {})
css_class = data[:class] || 'btn-clipboard btn-transparent' css_class = data[:class] || 'btn-clipboard btn-transparent'
title = data[:title] || 'Copy to clipboard' title = data[:title] || 'Copy to clipboard'
# This supports code in app/assets/javascripts/copy_to_clipboard.js that
# works around ClipboardJS limitations to allow the context-specific copy/pasting of plain text or GFM.
if text = data.delete(:text)
data[:clipboard_text] =
if gfm = data.delete(:gfm)
{ text: text, gfm: gfm }
else
text
end
end
target = data.delete(:target)
data[:clipboard_target] = target if target
data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data) data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data)
content_tag :button, content_tag :button,
icon('clipboard', 'aria-hidden': 'true'), icon('clipboard', 'aria-hidden': 'true'),
class: "btn #{css_class}", class: "btn #{css_class}",
......
module SystemNoteHelper
ICON_NAMES_BY_ACTION = {
'commit' => 'icon_commit',
'merge' => 'icon_merge',
'merged' => 'icon_merged',
'opened' => 'icon_status_open',
'closed' => 'icon_status_closed',
'time_tracking' => 'icon_stopwatch',
'assignee' => 'icon_user',
'title' => 'icon_pencil',
'task' => 'icon_check_square_o',
'label' => 'icon_tags',
'cross_reference' => 'icon_random',
'branch' => 'icon_code_fork',
'confidential' => 'icon_eye_slash',
'visible' => 'icon_eye',
'milestone' => 'icon_clock_o',
'discussion' => 'icon_comment_o',
'moved' => 'icon_arrow_circle_o_right'
}.freeze
def icon_for_system_note(note)
icon_name = ICON_NAMES_BY_ACTION[note.system_note_metadata&.action]
custom_icon(icon_name) if icon_name
end
end
This diff is collapsed.
This diff is collapsed.
...@@ -8,6 +8,7 @@ module Ci ...@@ -8,6 +8,7 @@ module Ci
belongs_to :owner, class_name: "User" belongs_to :owner, class_name: "User"
has_many :trigger_requests, dependent: :destroy has_many :trigger_requests, dependent: :destroy
has_one :trigger_schedule, dependent: :destroy
validates :token, presence: true, uniqueness: true validates :token, presence: true, uniqueness: true
......
module Ci
class TriggerSchedule < ActiveRecord::Base
extend Ci::Model
include Importable
acts_as_paranoid
belongs_to :project
belongs_to :trigger
delegate :ref, to: :trigger
validates :trigger, presence: { unless: :importing? }
validates :cron, cron: true, presence: { unless: :importing? }
validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? }
validates :ref, presence: { unless: :importing? }
before_save :set_next_run_at
def set_next_run_at
self.next_run_at = Gitlab::Ci::CronParser.new(cron, cron_timezone).next_time_from(Time.now)
end
def schedule_next_run!
save! # with set_next_run_at
rescue ActiveRecord::RecordInvalid
update_attribute(:next_run_at, nil) # update without validation
end
end
end
...@@ -7,6 +7,7 @@ class CommitStatus < ActiveRecord::Base ...@@ -7,6 +7,7 @@ class CommitStatus < ActiveRecord::Base
belongs_to :project belongs_to :project
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id
belongs_to :auto_canceled_by, class_name: 'Ci::Pipeline'
belongs_to :user belongs_to :user
delegate :commit, to: :pipeline delegate :commit, to: :pipeline
...@@ -137,6 +138,10 @@ class CommitStatus < ActiveRecord::Base ...@@ -137,6 +138,10 @@ class CommitStatus < ActiveRecord::Base
false false
end end
def auto_canceled?
canceled? && auto_canceled_by_id?
end
# Added in 9.0 to keep backward compatibility for projects exported in 8.17 # Added in 9.0 to keep backward compatibility for projects exported in 8.17
# and prior. # and prior.
def gl_project_id def gl_project_id
......
...@@ -76,6 +76,7 @@ module HasStatus ...@@ -76,6 +76,7 @@ module HasStatus
scope :canceled, -> { where(status: 'canceled') } scope :canceled, -> { where(status: 'canceled') }
scope :skipped, -> { where(status: 'skipped') } scope :skipped, -> { where(status: 'skipped') }
scope :manual, -> { where(status: 'manual') } scope :manual, -> { where(status: 'manual') }
scope :created_or_pending, -> { where(status: [:created, :pending]) }
scope :running_or_pending, -> { where(status: [:running, :pending]) } scope :running_or_pending, -> { where(status: [:running, :pending]) }
scope :finished, -> { where(status: [:success, :failed, :canceled]) } scope :finished, -> { where(status: [:success, :failed, :canceled]) }
scope :failed_or_canceled, -> { where(status: [:failed, :canceled]) } scope :failed_or_canceled, -> { where(status: [:failed, :canceled]) }
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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