Commit 07f6ab2b authored by Marin Jankovski's avatar Marin Jankovski

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce

parents 72111fad 27b98bea
......@@ -2,7 +2,6 @@ source 'https://rubygems.org'
gem 'rails', '4.2.8'
gem 'rails-deprecated_sanitizer', '~> 1.0.3'
gem 'bootsnap', '~> 1.0.0'
# Responders respond_to and respond_with
gem 'responders', '~> 2.0'
......@@ -123,6 +122,7 @@ gem 'asciidoctor', '~> 1.5.2'
gem 'asciidoctor-plantuml', '0.0.7'
gem 'rouge', '~> 2.0'
gem 'truncato', '~> 0.7.8'
gem 'bootstrap_form', '~> 2.7.0'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
......
......@@ -83,11 +83,10 @@ GEM
bindata (2.3.5)
binding_of_caller (0.7.2)
debug_inspector (>= 0.0.1)
bootsnap (1.0.0)
msgpack (~> 1.0)
bootstrap-sass (3.3.6)
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
bootstrap_form (2.7.0)
brakeman (3.6.1)
browser (2.2.0)
builder (3.2.3)
......@@ -353,7 +352,7 @@ GEM
grape-entity (0.6.0)
activesupport
multi_json (>= 1.3.2)
grpc (1.2.5)
grpc (1.4.0)
google-protobuf (~> 3.1)
googleauth (~> 0.5.1)
haml (4.0.7)
......@@ -464,7 +463,6 @@ GEM
minitest (5.7.0)
mmap2 (2.2.6)
mousetrap-rails (1.4.6)
msgpack (1.1.0)
multi_json (1.12.1)
multi_xml (0.6.0)
multipart-post (2.0.0)
......@@ -928,8 +926,8 @@ DEPENDENCIES
benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0)
binding_of_caller (~> 0.7.2)
bootsnap (~> 1.0.0)
bootstrap-sass (~> 3.3.0)
bootstrap_form (~> 2.7.0)
brakeman (~> 3.6.0)
browser (~> 2.2)
bullet (~> 5.5.0)
......
This diff is collapsed.
......@@ -17,7 +17,7 @@ export default {
methods: {
submit(e) {
e.preventDefault();
if (this.title.trim() === '') return;
if (this.title.trim() === '') return Promise.resolve();
this.error = false;
......@@ -29,7 +29,10 @@ export default {
assignees: [],
});
this.list.newIssue(issue)
eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel();
return this.list.newIssue(issue)
.then(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$refs.submitButton).enable();
......@@ -47,9 +50,6 @@ export default {
// Show error message
this.error = true;
});
eventHub.$emit(`scroll-board-list-${this.list.id}`);
this.cancel();
},
cancel() {
this.title = '';
......
......@@ -112,8 +112,7 @@ class List {
.then((resp) => {
const data = resp.json();
issue.id = data.iid;
})
.then(() => {
if (this.issuesSize > 1) {
const moveBeforeIid = this.issues[1].id;
gl.boardService.moveIssue(issue.id, null, null, null, moveBeforeIid);
......
......@@ -2,6 +2,7 @@
import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
......@@ -12,6 +13,10 @@ export default {
},
},
directives: {
tooltip,
},
components: {
loadingIcon,
},
......@@ -33,8 +38,6 @@ export default {
onClickAction(endpoint) {
this.isLoading = true;
$(this.$refs.tooltip).tooltip('destroy');
eventHub.$emit('postAction', endpoint);
},
......@@ -53,11 +56,11 @@ export default {
class="btn-group"
role="group">
<button
v-tooltip
type="button"
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip"
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container"
data-container="body"
data-toggle="dropdown"
ref="tooltip"
:title="title"
:aria-label="title"
:disabled="isLoading">
......
<script>
import tooltip from '../../vue_shared/directives/tooltip';
/**
* Renders the external url link in environments table.
*/
......@@ -10,6 +12,10 @@ export default {
},
},
directives: {
tooltip,
},
computed: {
title() {
return 'Open';
......@@ -19,7 +25,8 @@ export default {
</script>
<template>
<a
class="btn external-url has-tooltip"
v-tooltip
class="btn external-url"
data-container="body"
target="_blank"
rel="noopener noreferrer nofollow"
......
......@@ -2,6 +2,8 @@
/**
* Renders the Monitoring (Metrics) link in environments table.
*/
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
monitoringUrl: {
......@@ -10,6 +12,10 @@ export default {
},
},
directives: {
tooltip,
},
computed: {
title() {
return 'Monitoring';
......@@ -19,7 +25,8 @@ export default {
</script>
<template>
<a
class="btn monitoring-url has-tooltip hidden-xs hidden-sm"
v-tooltip
class="btn monitoring-url hidden-xs hidden-sm"
data-container="body"
rel="noopener noreferrer nofollow"
:href="monitoringUrl"
......
......@@ -5,6 +5,7 @@
*/
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
......@@ -14,6 +15,10 @@ export default {
},
},
directives: {
tooltip,
},
data() {
return {
isLoading: false,
......@@ -46,8 +51,9 @@ export default {
</script>
<template>
<button
v-tooltip
type="button"
class="btn stop-env-link has-tooltip hidden-xs hidden-sm"
class="btn stop-env-link hidden-xs hidden-sm"
data-container="body"
@click="onClick"
:disabled="isLoading"
......
......@@ -4,6 +4,7 @@
* Used in environments table.
*/
import terminalIconSvg from 'icons/_icon_terminal.svg';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
......@@ -14,6 +15,10 @@ export default {
},
},
directives: {
tooltip,
},
data() {
return {
terminalIconSvg,
......@@ -29,7 +34,8 @@ export default {
</script>
<template>
<a
class="btn terminal-button has-tooltip hidden-xs hidden-sm"
v-tooltip
class="btn terminal-button hidden-xs hidden-sm"
data-container="body"
:title="title"
:aria-label="title"
......
......@@ -40,6 +40,10 @@ class FilteredSearchManager {
return [];
})
.then((searches) => {
if (!searches) {
return;
}
// Put any searches that may have come in before
// we fetched the saved searches ahead of the already saved ones
const resultantSearches = this.recentSearchesStore.setRecentSearches(
......
......@@ -47,8 +47,8 @@ export default class GroupsStore {
// Map groups to an object
groups.map((group) => {
mappedGroups[group.id] = group;
mappedGroups[group.id].subGroups = {};
mappedGroups[`id${group.id}`] = group;
mappedGroups[`id${group.id}`].subGroups = {};
return group;
});
......@@ -56,26 +56,27 @@ export default class GroupsStore {
const currentGroup = mappedGroups[key];
if (currentGroup.parentId) {
// If the group is not at the root level, add it to its parent array of subGroups.
const findParentGroup = mappedGroups[currentGroup.parentId];
const findParentGroup = mappedGroups[`id${currentGroup.parentId}`];
if (findParentGroup) {
mappedGroups[currentGroup.parentId].subGroups[currentGroup.id] = currentGroup;
mappedGroups[currentGroup.parentId].isOpen = true; // Expand group if it has subgroups
mappedGroups[`id${currentGroup.parentId}`].subGroups[`id${currentGroup.id}`] = currentGroup;
mappedGroups[`id${currentGroup.parentId}`].isOpen = true; // Expand group if it has subgroups
} else if (parentGroup && parentGroup.id === currentGroup.parentId) {
tree[currentGroup.id] = currentGroup;
tree[`id${currentGroup.id}`] = currentGroup;
} else {
// Means the groups hast no direct parent.
// Save for later processing, we will add them to its corresponding base group
// No parent found. We save it for later processing
orphans.push(currentGroup);
// Add to tree to preserve original order
tree[`id${currentGroup.id}`] = currentGroup;
}
} else {
// If the group is at the root level, add it to first level elements array.
tree[currentGroup.id] = currentGroup;
// If the group is at the top level, add it to first level elements array.
tree[`id${currentGroup.id}`] = currentGroup;
}
return key;
});
// Hopefully this array will be empty for most cases
if (orphans.length) {
orphans.map((orphan) => {
let found = false;
......@@ -83,11 +84,23 @@ export default class GroupsStore {
Object.keys(tree).map((key) => {
const group = tree[key];
if (currentOrphan.fullPath.lastIndexOf(group.fullPath) === 0) {
if (
group &&
currentOrphan.fullPath.lastIndexOf(group.fullPath) === 0 &&
// Make sure the currently selected orphan is not the same as the group
// we are checking here otherwise it will end up in an infinite loop
currentOrphan.id !== group.id
) {
group.subGroups[currentOrphan.id] = currentOrphan;
group.isOpen = true;
currentOrphan.isOrphan = true;
found = true;
// Delete if group was put at the top level. If not the group will be displayed twice.
if (tree[`id${currentOrphan.id}`]) {
delete tree[`id${currentOrphan.id}`];
}
}
return key;
......@@ -95,7 +108,8 @@ export default class GroupsStore {
if (!found) {
currentOrphan.isOrphan = true;
tree[currentOrphan.id] = currentOrphan;
tree[`id${currentOrphan.id}`] = currentOrphan;
}
return orphan;
......@@ -140,7 +154,7 @@ export default class GroupsStore {
// eslint-disable-next-line class-methods-use-this
removeGroup(group, collection) {
Vue.delete(collection, group.id);
Vue.delete(collection, `id${group.id}`);
}
// eslint-disable-next-line class-methods-use-this
......
......@@ -47,7 +47,8 @@
ref="textarea"
slot="textarea"
placeholder="Write a comment or drag your files here..."
@keydown.meta.enter="updateIssuable">
@keydown.meta.enter="updateIssuable"
@keydown.ctrl.enter="updateIssuable">
</textarea>
</markdown-field>
</div>
......
<script>
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
import tooltip from '../../../vue_shared/directives/tooltip';
export default {
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
props: {
formState: {
type: Object,
......@@ -71,9 +71,9 @@
data-placeholder="Move to a different project" />
</div>
<span
v-tooltip
data-placement="auto top"
title="Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location."
ref="tooltip">
title="Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.">
<i
class="fa fa-question-circle"
aria-hidden="true">
......
......@@ -26,6 +26,7 @@
placeholder="Issue title"
aria-label="Issue title"
v-model="formState.title"
@keydown.meta.enter="updateIssuable" />
@keydown.meta.enter="updateIssuable"
@keydown.ctrl.enter="updateIssuable" />
</fieldset>
</template>
......@@ -86,18 +86,25 @@
// This is required to handle non-unicode characters in hash
hash = decodeURIComponent(hash);
var fixedTabs = document.querySelector('.js-tabs-affix');
var fixedNav = document.querySelector('.navbar-gitlab');
var adjustment = 0;
if (fixedNav) adjustment -= fixedNav.offsetHeight;
// scroll to user-generated markdown anchor if we cannot find a match
if (document.getElementById(hash) === null) {
var target = document.getElementById('user-content-' + hash);
if (target && target.scrollIntoView) {
target.scrollIntoView(true);
window.scrollBy(0, adjustment);
}
} else {
// only adjust for fixedTabs when not targeting user-generated content
var fixedTabs = document.querySelector('.js-tabs-affix');
if (fixedTabs) {
window.scrollBy(0, -fixedTabs.offsetHeight);
adjustment -= fixedTabs.offsetHeight;
}
window.scrollBy(0, adjustment);
}
};
......
This diff is collapsed.
......@@ -155,7 +155,10 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
scrollToElement(container) {
if (location.hash) {
const offset = -$('.js-tabs-affix').outerHeight();
const offset = 0 - (
$('.navbar-gitlab').outerHeight() +
$('.js-tabs-affix').outerHeight()
);
const $el = $(`${container} ${location.hash}:not(.match)`);
if ($el.length) {
$.scrollTo($el[0], { offset });
......@@ -291,7 +294,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
// Scroll any linked note into view
// Similar to `toggler_behavior` in the discussion tab
const hash = window.gl.utils.getLocationHash();
const anchor = hash && $container.find(`[id="${hash}"]`);
const anchor = hash && $container.find(`.note[id="${hash}"]`);
if (anchor && anchor.length > 0) {
const notesContent = anchor.closest('.notes_content');
const lineType = notesContent.hasClass('new') ? 'new' : 'old';
......@@ -301,6 +304,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
forceShow: true,
});
anchor[0].scrollIntoView();
window.gl.utils.handleLocationHash();
// We have multiple elements on the page with `#note_xxx`
// (discussion and diff tabs) and `:target` only applies to the first
anchor.addClass('target');
......
This diff is collapsed.
......@@ -3,7 +3,7 @@
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltipMixin from '../../vue_shared/mixins/tooltip';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
......@@ -28,12 +28,12 @@ export default {
required: false,
},
},
directives: {
tooltip,
},
components: {
loadingIcon,
},
mixins: [
tooltipMixin,
],
data() {
return {
isLoading: false,
......@@ -58,7 +58,6 @@ export default {
makeRequest() {
this.isLoading = true;
$(this.$refs.tooltip).tooltip('destroy');
eventHub.$emit('postAction', this.endpoint);
},
},
......@@ -67,6 +66,7 @@ export default {
<template>
<button
v-tooltip
type="button"
@click="onClick"
:class="buttonClass"
......@@ -74,7 +74,6 @@ export default {
:aria-label="title"
data-container="body"
data-placement="top"
ref="tooltip"
:disabled="isLoading">
<i
:class="iconClass"
......
<script>
import getActionIcon from '../../../vue_shared/ci_action_icons';
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
* Renders either a cancel, retry or play icon pointing to the given path.
......@@ -29,9 +29,9 @@
},
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
computed: {
actionIconSvg() {
......@@ -46,12 +46,11 @@
</script>
<template>
<a
v-tooltip
:data-method="actionMethod"
:title="tooltipText"
:href="link"
ref="tooltip"
class="ci-action-icon-container"
data-toggle="tooltip"
data-container="body">
<i
......
<script>
import getActionIcon from '../../../vue_shared/ci_action_icons';
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
* Renders either a cancel, retry or play icon pointing to the given path.
......@@ -29,9 +29,9 @@
},
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
computed: {
actionIconSvg() {
......@@ -42,13 +42,12 @@
</script>
<template>
<a
v-tooltip
:data-method="actionMethod"
:title="tooltipText"
:href="link"
ref="tooltip"
rel="nofollow"
class="ci-action-icon-wrapper js-ci-status-icon"
data-toggle="tooltip"
data-container="body"
v-html="actionIconSvg"
aria-label="Job's action">
......
<script>
import jobNameComponent from './job_name_component.vue';
import jobComponent from './job_component.vue';
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
* Renders the dropdown for the pipeline graph.
......@@ -34,9 +34,9 @@
},
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
components: {
jobComponent,
......@@ -53,12 +53,12 @@
<template>
<div>
<button
v-tooltip
type="button"
data-toggle="dropdown"
data-container="body"
class="dropdown-menu-toggle build-content"
:title="tooltipText"
ref="tooltip">
:title="tooltipText">
<job-name-component
:name="job.name"
......
......@@ -2,7 +2,7 @@
import actionComponent from './action_component.vue';
import dropdownActionComponent from './dropdown_action_component.vue';
import jobNameComponent from './job_name_component.vue';
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
import tooltip from '../../../vue_shared/directives/tooltip';
/**
* Renders the badge for the pipeline graph and the job's dropdown.
......@@ -54,9 +54,9 @@
jobNameComponent,
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
computed: {
tooltipText() {
......@@ -77,12 +77,11 @@
<template>
<div>
<a
v-tooltip
v-if="job.status.details_path"
:href="job.status.details_path"
:title="tooltipText"
:class="cssClassJobName"
ref="tooltip"
data-toggle="tooltip"
data-container="body">
<job-name-component
......@@ -93,10 +92,9 @@
<div
v-else
v-tooltip
:title="tooltipText"
:class="cssClassJobName"
ref="tooltip"
data-toggle="tooltip"
data-container="body">
<job-name-component
......
<script>
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import tooltipMixin from '../../vue_shared/mixins/tooltip';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
......@@ -12,9 +12,9 @@ export default {
components: {
userAvatarLink,
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
computed: {
user() {
return this.pipeline.user;
......@@ -45,16 +45,16 @@ export default {
<div class="label-container">
<span
v-if="pipeline.flags.latest"
v-tooltip
class="js-pipeline-url-latest label label-success"
title="Latest pipeline for this branch"
ref="tooltip">
title="Latest pipeline for this branch">
latest
</span>
<span
v-if="pipeline.flags.yaml_errors"
v-tooltip
class="js-pipeline-url-yaml label label-danger"
:title="pipeline.yaml_errors"
ref="tooltip">
:title="pipeline.yaml_errors">
yaml invalid
</span>
<span
......
......@@ -4,6 +4,7 @@
import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
......@@ -12,6 +13,9 @@
required: true,
},
},
directives: {
tooltip,
},
components: {
loadingIcon,
},
......@@ -25,8 +29,6 @@
onClickAction(endpoint) {
this.isLoading = true;
$(this.$refs.tooltip).tooltip('destroy');
eventHub.$emit('postAction', endpoint);
},
......@@ -43,13 +45,13 @@
<template>
<div class="btn-group">
<button
v-tooltip
type="button"
class="dropdown-new btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
class="dropdown-new btn btn-default js-pipeline-dropdown-manual-actions"
title="Manual job"
data-toggle="dropdown"
data-placement="top"
aria-label="Manual job"
ref="tooltip"
:disabled="isLoading">
<span v-html="playIconSvg"></span>
<i
......
<script>
import tooltipMixin from '../../vue_shared/mixins/tooltip';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
......@@ -8,9 +8,9 @@
required: true,
},
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
};
</script>
<template>
......@@ -18,12 +18,12 @@
class="btn-group"
role="group">
<button
v-tooltip
class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download"
title="Artifacts"
data-placement="top"
data-toggle="dropdown"
aria-label="Artifacts"
ref="tooltip">
aria-label="Artifacts">
<i
class="fa fa-download"
aria-hidden="true">
......
......@@ -16,7 +16,7 @@
/* global Flash */
import { borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tooltipMixin from '../../vue_shared/mixins/tooltip';
import tooltip from '../../vue_shared/directives/tooltip';
export default {
props: {
......@@ -32,9 +32,9 @@ export default {
},
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
data() {
return {
......@@ -132,7 +132,7 @@ export default {
<template>
<div class="dropdown">
<button
ref="tooltip"
v-tooltip
:class="triggerButtonClass"
@click="onClickStage"
class="mini-pipeline-graph-dropdown-toggle js-builds-dropdown-button"
......
<script>
import iconTimerSvg from 'icons/_icon_timer.svg';
import '../../lib/utils/datetime_utility';
import tooltipMixin from '../../vue_shared/mixins/tooltip';
import tooltip from '../../vue_shared/directives/tooltip';
import timeagoMixin from '../../vue_shared/mixins/timeago';
export default {
......@@ -16,9 +16,11 @@
},
},
mixins: [
tooltipMixin,
timeagoMixin,
],
directives: {
tooltip,
},
data() {
return {
iconTimerSvg,
......@@ -81,7 +83,7 @@
</i>
<time
ref="tooltip"
v-tooltip
data-placement="top"
data-container="body"
:title="tooltipTitle(finishedTime)">
......
export default {
EMPTY: 'empty',
LOADING: 'loading',
LIST: 'list',
};
import PrometheusMetrics from './prometheus_metrics';
$(() => {
const prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
prometheusMetrics.loadActiveMetrics();
});
import PANEL_STATE from './constants';
export default class PrometheusMetrics {
constructor(wrapperSelector) {
this.backOffRequestCounter = 0;
this.$wrapper = $(wrapperSelector);
this.$monitoredMetricsPanel = this.$wrapper.find('.js-panel-monitored-metrics');
this.$monitoredMetricsCount = this.$monitoredMetricsPanel.find('.js-monitored-count');
this.$monitoredMetricsLoading = this.$monitoredMetricsPanel.find('.js-loading-metrics');
this.$monitoredMetricsEmpty = this.$monitoredMetricsPanel.find('.js-empty-metrics');
this.$monitoredMetricsList = this.$monitoredMetricsPanel.find('.js-metrics-list');
this.$missingEnvVarPanel = this.$wrapper.find('.js-panel-missing-env-vars');
this.$panelToggle = this.$missingEnvVarPanel.find('.js-panel-toggle');
this.$missingEnvVarMetricCount = this.$missingEnvVarPanel.find('.js-env-var-count');
this.$missingEnvVarMetricsList = this.$missingEnvVarPanel.find('.js-missing-var-metrics-list');
this.activeMetricsEndpoint = this.$monitoredMetricsPanel.data('active-metrics');
this.$panelToggle.on('click', e => this.handlePanelToggle(e));
}
/* eslint-disable class-methods-use-this */
handlePanelToggle(e) {
const $toggleBtn = $(e.currentTarget);
const $currentPanelBody = $toggleBtn.closest('.panel').find('.panel-body');
$currentPanelBody.toggleClass('hidden');
if ($toggleBtn.hasClass('fa-caret-down')) {
$toggleBtn.removeClass('fa-caret-down').addClass('fa-caret-right');
} else {
$toggleBtn.removeClass('fa-caret-right').addClass('fa-caret-down');
}
}
showMonitoringMetricsPanelState(stateName) {
switch (stateName) {
case PANEL_STATE.LOADING:
this.$monitoredMetricsLoading.removeClass('hidden');
this.$monitoredMetricsEmpty.addClass('hidden');
this.$monitoredMetricsList.addClass('hidden');
break;
case PANEL_STATE.LIST:
this.$monitoredMetricsLoading.addClass('hidden');
this.$monitoredMetricsEmpty.addClass('hidden');
this.$monitoredMetricsList.removeClass('hidden');
break;
default:
this.$monitoredMetricsLoading.addClass('hidden');
this.$monitoredMetricsEmpty.removeClass('hidden');
this.$monitoredMetricsList.addClass('hidden');
break;
}
}
populateActiveMetrics(metrics) {
let totalMonitoredMetrics = 0;
let totalMissingEnvVarMetrics = 0;
metrics.forEach((metric) => {
this.$monitoredMetricsList.append(`<li>${metric.group}<span class="badge">${metric.active_metrics}</span></li>`);
totalMonitoredMetrics += metric.active_metrics;
if (metric.metrics_missing_requirements > 0) {
this.$missingEnvVarMetricsList.append(`<li>${metric.group}</li>`);
totalMissingEnvVarMetrics += 1;
}
});
this.$monitoredMetricsCount.text(totalMonitoredMetrics);
this.showMonitoringMetricsPanelState(PANEL_STATE.LIST);
if (totalMissingEnvVarMetrics > 0) {
this.$missingEnvVarPanel.removeClass('hidden');
this.$missingEnvVarPanel.find('.flash-container').off('click');
this.$missingEnvVarMetricCount.text(totalMissingEnvVarMetrics);
}
}
loadActiveMetrics() {
this.showMonitoringMetricsPanelState(PANEL_STATE.LOADING);
gl.utils.backOff((next, stop) => {
$.getJSON(this.activeMetricsEndpoint)
.done((res) => {
if (res && res.success) {
stop(res);
} else {
this.backOffRequestCounter = this.backOffRequestCounter += 1;
if (this.backOffRequestCounter < 3) {
next();
} else {
stop(res);
}
}
})
.fail(stop);
})
.then((res) => {
if (res && res.data && res.data.length) {
this.populateActiveMetrics(res.data);
} else {
this.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY);
}
})
.catch(() => {
this.showMonitoringMetricsPanelState(PANEL_STATE.EMPTY);
});
}
}
......@@ -10,8 +10,6 @@ import Cookies from 'js-cookie';
this.$sidebarInner = this.sidebar.find('.issuable-sidebar');
this.$navGitlab = $('.navbar-gitlab');
this.$layoutNav = $('.layout-nav');
this.$subScroll = $('.sub-nav-scroll');
this.$rightSidebar = $('.js-right-sidebar');
this.removeListeners();
......@@ -215,7 +213,7 @@ import Cookies from 'js-cookie';
};
Sidebar.prototype.setSidebarHeight = function() {
const $navHeight = this.$navGitlab.outerHeight() + this.$layoutNav.outerHeight() + (this.$subScroll ? this.$subScroll.outerHeight() : 0);
const $navHeight = this.$navGitlab.outerHeight();
const diff = $navHeight - $(window).scrollTop();
if (diff > 0) {
this.$rightSidebar.outerHeight($(window).height() - diff);
......
......@@ -4,7 +4,7 @@ function expandSectionParent($section, $content) {
}
function expandSection($section) {
$section.find('.js-settings-toggle').text('Close');
$section.find('.js-settings-toggle').text('Collapse');
const $content = $section.find('.settings-content');
$content.addClass('expanded').off('scroll.expandSection').scrollTop(0);
......
......@@ -643,7 +643,7 @@ UsersSelect.prototype.formatResult = function(user) {
} else {
avatar = gon.default_avatar_url;
}
return "<div class='user-result " + (!user.username ? 'no-username' : void 0) + "'> <div class='user-image'><img class='avatar s24' src='" + avatar + "'></div> <div class='user-name'>" + user.name + "</div> <div class='user-username'>" + (user.username || "") + "</div> </div>";
return "<div class='user-result " + (!user.username ? 'no-username' : void 0) + "'> <div class='user-image'><img class='avatar avatar-inline s32' src='" + avatar + "'></div> <div class='user-name dropdown-menu-user-full-name'>" + user.name + "</div> <div class='user-username dropdown-menu-user-username'>" + ("@" + user.username || "") + "</div> </div>";
};
UsersSelect.prototype.formatSelection = function(user) {
......
......@@ -2,7 +2,7 @@
import ciIconBadge from './ci_badge_link.vue';
import loadingIcon from './loading_icon.vue';
import timeagoTooltip from './time_ago_tooltip.vue';
import tooltipMixin from '../mixins/tooltip';
import tooltip from '../directives/tooltip';
import userAvatarImage from './user_avatar/user_avatar_image.vue';
/**
......@@ -47,9 +47,9 @@ export default {
},
},
mixins: [
tooltipMixin,
],
directives: {
tooltip,
},
components: {
ciIconBadge,
......@@ -90,10 +90,10 @@ export default {
<template v-if="user">
<a
v-tooltip
:href="user.path"
:title="user.email"
class="js-user-link commit-committer-link"
ref="tooltip">
class="js-user-link commit-committer-link">
<user-avatar-image
:img-src="user.avatar_url"
......
<script>
import tooltipMixin from '../../mixins/tooltip';
import tooltip from '../../directives/tooltip';
import toolbarButton from './toolbar_button.vue';
export default {
mixins: [
tooltipMixin,
],
props: {
previewMarkdown: {
type: Boolean,
required: true,
},
},
directives: {
tooltip,
},
components: {
toolbarButton,
},
......@@ -94,13 +94,13 @@
</div>
<div class="toolbar-group">
<button
v-tooltip
aria-label="Go full screen"
class="toolbar-btn js-zen-enter"
data-container="body"
tabindex="-1"
title="Go full screen"
type="button"
ref="tooltip">
type="button">
<i
aria-hidden="true"
class="fa fa-arrows-alt fa-fw">
......
<script>
import tooltipMixin from '../../mixins/tooltip';
import tooltip from '../../directives/tooltip';
export default {
mixins: [
tooltipMixin,
],
props: {
buttonTitle: {
type: String,
......@@ -29,6 +26,9 @@
default: false,
},
},
directives: {
tooltip,
},
computed: {
iconClass() {
return `fa-${this.icon}`;
......@@ -39,10 +39,10 @@
<template>
<button
v-tooltip
type="button"
class="toolbar-btn js-md hidden-xs"
tabindex="-1"
ref="tooltip"
data-container="body"
:data-md-tag="tag"
:data-md-block="tagBlock"
......
<script>
import tooltipMixin from '../mixins/tooltip';
import tooltip from '../directives/tooltip';
import timeagoMixin from '../mixins/timeago';
import '../../lib/utils/datetime_utility';
......@@ -28,19 +28,21 @@ export default {
},
mixins: [
tooltipMixin,
timeagoMixin,
],
directives: {
tooltip,
},
};
</script>
<template>
<time
v-tooltip
:class="cssClass"
class="js-vue-timeago"
:title="tooltipTitle(time)"
:data-placement="tooltipPlacement"
data-container="body"
ref="tooltip">
data-container="body">
{{timeFormated(time)}}
</time>
</template>
......@@ -16,11 +16,10 @@
*/
import defaultAvatarUrl from 'images/no_avatar.png';
import TooltipMixin from '../../mixins/tooltip';
import tooltip from '../../directives/tooltip';
export default {
name: 'UserAvatarImage',
mixins: [TooltipMixin],
props: {
imgSrc: {
type: String,
......@@ -53,6 +52,9 @@ export default {
default: 'top',
},
},
directives: {
tooltip,
},
computed: {
tooltipContainer() {
return this.tooltipText ? 'body' : null;
......@@ -72,6 +74,7 @@ export default {
<template>
<img
v-tooltip
class="avatar"
:class="[avatarSizeClass, cssClasses]"
:src="imageSource"
......@@ -81,6 +84,5 @@ export default {
:data-container="tooltipContainer"
:data-placement="tooltipPlacement"
:title="tooltipText"
ref="tooltip"
/>
</template>
export default {
bind(el) {
$(el).tooltip();
},
componentUpdated(el) {
$(el).tooltip('fixTitle');
},
unbind(el) {
$(el).tooltip('destroy');
},
};
export default {
mounted() {
$(this.$refs.tooltip).tooltip();
},
updated() {
$(this.$refs.tooltip).tooltip('fixTitle');
},
beforeDestroy() {
$(this.$refs.tooltip).tooltip('destroy');
},
};
......@@ -395,6 +395,11 @@
.dropdown-menu-align-right {
left: auto;
right: 0;
margin-top: -5px;
@media (max-width: $screen-xs-max) {
left: 0;
}
}
.dropdown-menu-selectable {
......
......@@ -12,6 +12,12 @@
&.readme-holder {
margin: $gl-padding 0;
&.limited-width-container .file-content {
max-width: $limited-layout-width-sm;
margin-left: auto;
margin-right: auto;
}
}
table {
......
......@@ -346,6 +346,7 @@ header {
width: auto;
min-width: 140px;
margin-top: -5px;
left: auto;
.current-user {
padding: 5px 18px;
......
......@@ -53,7 +53,7 @@ body {
}
&.limit-container-width-sm {
max-width: 790px;
max-width: $limited-layout-width-sm;
}
}
......
......@@ -161,6 +161,7 @@ $progress-color: #c0392b;
$header-height: 50px;
$fixed-layout-width: 1280px;
$limited-layout-width: 990px;
$limited-layout-width-sm: 790px;
$gl-avatar-size: 40px;
$error-exclamation-point: $red-500;
$border-radius-default: 3px;
......
......@@ -83,6 +83,7 @@
.avatar {
float: none;
margin-right: 0;
}
}
......
......@@ -11,7 +11,9 @@
.commit-box,
.info-well,
.commit-ci-menu,
.files-changed {
.files-changed,
.limited-header-width,
.limited-width-notes {
@extend .fixed-width-container;
}
......
......@@ -136,10 +136,6 @@
width: 250px;
}
@media (min-width: $screen-md-min) {
width: 350px;
}
&.input-short {
@media (min-width: $screen-md-min) {
width: 170px;
......
......@@ -419,7 +419,7 @@
.commit {
margin: 0;
padding: 10px 0;
padding: 10px;
list-style: none;
&:hover {
......
......@@ -133,7 +133,7 @@
overflow: hidden;
display: inline-block;
white-space: nowrap;
vertical-align: top;
vertical-align: middle;
text-overflow: ellipsis;
}
......
......@@ -126,3 +126,66 @@
margin-left: 5px;
}
}
.prometheus-metrics-monitoring {
.panel {
.panel-toggle {
width: 14px;
}
.badge {
font-size: inherit;
}
.panel-heading .badge-count {
color: $white-light;
background: $common-gray-dark;
}
.panel-body {
padding: 0;
}
.flash-container {
margin-bottom: 0;
cursor: default;
.flash-notice {
border-radius: 0;
}
}
}
.loading-metrics,
.empty-metrics {
padding: 30px 10px;
p,
.btn {
margin-top: 10px;
margin-bottom: 0;
}
}
.loading-metrics .metrics-load-spinner {
color: $loading-color;
}
.metrics-list {
margin-bottom: 0;
li {
padding: $gl-padding;
.badge {
margin-left: 5px;
background: $badge-bg;
}
}
/* Ensure we don't add border if there's only single li */
li + li {
border-top: 1px solid $border-color;
}
}
}
......@@ -40,6 +40,10 @@ class ApplicationController < ActionController::Base
render_404
end
rescue_from(ActionController::UnknownFormat) do
render_404
end
rescue_from Gitlab::Access::AccessDeniedError do |exception|
render_403
end
......
......@@ -22,6 +22,22 @@ class Projects::DeploymentsController < Projects::ApplicationController
render_404
end
def additional_metrics
return render_404 unless deployment.has_additional_metrics?
respond_to do |format|
format.json do
metrics = deployment.additional_metrics
if metrics.any?
render json: metrics
else
head :no_content
end
end
end
end
private
def deployment
......
......@@ -129,6 +129,16 @@ class Projects::EnvironmentsController < Projects::ApplicationController
end
end
def additional_metrics
respond_to do |format|
format.json do
additional_metrics = environment.additional_metrics || {}
render json: additional_metrics, status: additional_metrics.any? ? :ok : :no_content
end
end
end
private
def verify_api_request!
......
......@@ -575,10 +575,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def merge_request_params
params.require(:merge_request)
.permit(merge_request_params_ce)
.permit(merge_request_params_attributes)
end
def merge_request_params_ce
def merge_request_params_attributes
[
:assignee_id,
:description,
......@@ -598,7 +598,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def merge_params
params.permit(:should_remove_source_branch, :commit_message)
params.permit(merge_params_attributes)
end
def merge_params_attributes
[:should_remove_source_branch, :commit_message]
end
# Make sure merge requests created before 8.0
......
......@@ -135,7 +135,12 @@ class Projects::PipelinesController < Projects::ApplicationController
@charts[:week] = Ci::Charts::WeekChart.new(project)
@charts[:month] = Ci::Charts::MonthChart.new(project)
@charts[:year] = Ci::Charts::YearChart.new(project)
@charts[:build_times] = Ci::Charts::BuildTime.new(project)
@charts[:pipeline_times] = Ci::Charts::PipelineTime.new(project)
@counts = {}
@counts[:total] = @project.pipelines.count(:all)
@counts[:success] = @project.pipelines.success.count(:all)
@counts[:failed] = @project.pipelines.failed.count(:all)
end
private
......
class Projects::PrometheusController < Projects::ApplicationController
before_action :authorize_read_project!
before_action :require_prometheus_metrics!
def active_metrics
respond_to do |format|
format.json do
matched_metrics = project.prometheus_service.matched_metrics || {}
if matched_metrics.any?
render json: matched_metrics
else
head :no_content
end
end
end
end
private
def require_prometheus_metrics!
render_404 unless project.prometheus_service.present?
end
end
......@@ -41,7 +41,7 @@ class IssuesFinder < IssuableFinder
def self.not_restricted_by_confidentiality(user)
return Issue.where('issues.confidential IS NOT TRUE') if user.blank?
return Issue.all if user.admin?
return Issue.all if user.full_private_access?
Issue.where('
issues.confidential IS NOT TRUE
......
......@@ -85,20 +85,20 @@ module CommitsHelper
if @path.blank?
return link_to(
"Browse Files",
_("Browse Files"),
namespace_project_tree_path(project.namespace, project, commit),
class: "btn btn-default"
)
elsif @repo.blob_at(commit.id, @path)
return link_to(
"Browse File",
_("Browse File"),
namespace_project_blob_path(project.namespace, project,
tree_join(commit.id, @path)),
class: "btn btn-default"
)
elsif @path.present?
return link_to(
"Browse Directory",
_("Browse Directory"),
namespace_project_tree_path(project.namespace, project,
tree_join(commit.id, @path)),
class: "btn btn-default"
......
......@@ -17,13 +17,10 @@ module GraphHelper
ids.zip(parent_spaces)
end
def success_ratio(success_builds, failed_builds)
failed_builds = failed_builds.count(:all)
success_builds = success_builds.count(:all)
def success_ratio(counts)
return 100 if counts[:failed].zero?
return 100 if failed_builds.zero?
ratio = (success_builds.to_f / (success_builds + failed_builds)) * 100
ratio = (counts[:success].to_f / (counts[:success] + counts[:failed])) * 100
ratio.to_i
end
end
......@@ -15,7 +15,7 @@ module GroupsHelper
@has_group_title = true
full_title = ''
group.ancestors.each do |parent|
group.ancestors.reverse.each do |parent|
full_title += link_to(simple_sanitize(parent.name), group_path(parent), class: 'group-path hidable')
full_title += '<span class="hidable"> / </span>'.html_safe
end
......
......@@ -4,4 +4,14 @@ module UsersHelper
title: user.email,
class: 'has-tooltip commit-committer-link')
end
def user_email_help_text(user)
return 'We also use email for avatar detection if no avatar is uploaded.' unless user.unconfirmed_email.present?
confirmation_link = link_to 'Resend confirmation e-mail', user_confirmation_path(user: { email: @user.unconfirmed_email }), method: :post
h('Please click the link in the confirmation email before continuing. It was sent to ') +
content_tag(:strong) { user.unconfirmed_email } + h('.') +
content_tag(:p) { confirmation_link }
end
end
......@@ -114,6 +114,17 @@ class Deployment < ActiveRecord::Base
project.monitoring_service.deployment_metrics(self)
end
def has_additional_metrics?
project.prometheus_service.present?
end
def additional_metrics
return {} unless project.prometheus_service.present?
metrics = project.prometheus_service.additional_deployment_metrics(self)
metrics&.merge(deployment_time: created_at.to_i) || {}
end
private
def ref_path
......
......@@ -157,6 +157,16 @@ class Environment < ActiveRecord::Base
project.monitoring_service.environment_metrics(self) if has_metrics?
end
def has_additional_metrics?
project.prometheus_service.present? && available? && last_deployment.present?
end
def additional_metrics
if has_additional_metrics?
project.prometheus_service.additional_environment_metrics(self)
end
end
# An environment name is not necessarily suitable for use in URLs, DNS
# or other third-party contexts, so provide a slugified version. A slug has
# the following properties:
......
......@@ -350,7 +350,10 @@ class Project < ActiveRecord::Base
project.run_after_commit { add_import_job }
end
after_transition started: :finished, do: :reset_cache_and_import_attrs
after_transition started: :finished do |project, _|
project.reset_cache_and_import_attrs
project.perform_housekeeping
end
end
class << self
......@@ -510,6 +513,18 @@ class Project < ActiveRecord::Base
remove_import_data
end
def perform_housekeeping
return unless repo_exists?
run_after_commit do
begin
Projects::HousekeepingService.new(self).execute
rescue Projects::HousekeepingService::LeaseTaken => e
Rails.logger.info("Could not perform housekeeping for project #{self.path_with_namespace} (#{self.id}): #{e}")
end
end
end
def remove_import_data
import_data&.destroy
end
......
......@@ -90,7 +90,7 @@ class ProjectFeature < ActiveRecord::Base
when DISABLED
false
when PRIVATE
user && (project.team.member?(user) || user.admin?)
user && (project.team.member?(user) || user.full_private_access?)
when ENABLED
true
else
......
......@@ -28,17 +28,6 @@ class PrometheusService < MonitoringService
'Prometheus monitoring'
end
def help
<<-MD.strip_heredoc
Retrieves the Kubernetes node metrics `container_cpu_usage_seconds_total`
and `container_memory_usage_bytes` from the configured Prometheus server.
If you are not using [Auto-Deploy](https://docs.gitlab.com/ee/ci/autodeploy/index.html)
or have set up your own Prometheus server, an `environment` label is required on each metric to
[identify the Environment](https://docs.gitlab.com/ce/user/project/integrations/prometheus.html#metrics-and-labels).
MD
end
def self.to_param
'prometheus'
end
......@@ -50,6 +39,7 @@ class PrometheusService < MonitoringService
name: 'api_url',
title: 'API URL',
placeholder: 'Prometheus API Base URL, like http://prometheus.example.com/',
help: 'By default, Prometheus listens on ‘http://localhost:9090’. It’s not recommended to change the default address and port as this might affect or conflict with other services running on the GitLab server.',
required: true
}
]
......@@ -65,23 +55,34 @@ class PrometheusService < MonitoringService
end
def environment_metrics(environment)
with_reactive_cache(Gitlab::Prometheus::Queries::EnvironmentQuery.name, environment.id, &:itself)
with_reactive_cache(Gitlab::Prometheus::Queries::EnvironmentQuery.name, environment.id, &method(:rename_data_to_metrics))
end
def deployment_metrics(deployment)
metrics = with_reactive_cache(Gitlab::Prometheus::Queries::DeploymentQuery.name, deployment.id, &:itself)
metrics = with_reactive_cache(Gitlab::Prometheus::Queries::DeploymentQuery.name, deployment.id, &method(:rename_data_to_metrics))
metrics&.merge(deployment_time: created_at.to_i) || {}
end
def additional_environment_metrics(environment)
with_reactive_cache(Gitlab::Prometheus::Queries::AdditionalMetricsEnvironmentQuery.name, environment.id, &:itself)
end
def additional_deployment_metrics(deployment)
with_reactive_cache(Gitlab::Prometheus::Queries::AdditionalMetricsDeploymentQuery.name, deployment.id, &:itself)
end
def matched_metrics
with_reactive_cache(Gitlab::Prometheus::Queries::MatchedMetricsQuery.name, &:itself)
end
# Cache metrics for specific environment
def calculate_reactive_cache(query_class_name, *args)
return unless active? && project && !project.pending_delete?
metrics = Kernel.const_get(query_class_name).new(client).query(*args)
data = Kernel.const_get(query_class_name).new(client).query(*args)
{
success: true,
metrics: metrics,
data: data,
last_update: Time.now.utc
}
rescue Gitlab::PrometheusError => err
......@@ -91,4 +92,11 @@ class PrometheusService < MonitoringService
def client
@prometheus ||= Gitlab::PrometheusClient.new(api_url: api_url)
end
private
def rename_data_to_metrics(metrics)
metrics[:metrics] = metrics.delete :data
metrics
end
end
......@@ -984,6 +984,12 @@ class User < ActiveRecord::Base
self.admin = (new_level == 'admin')
end
# Does the user have access to all private groups & projects?
# Overridden in EE to also check auditor?
def full_private_access?
admin?
end
def update_two_factor_requirement
periods = expanded_groups_requiring_two_factor_authentication.pluck(:two_factor_grace_period)
......
......@@ -3,7 +3,7 @@
.avatar-container.s70.group-avatar
= image_tag group_icon(@group), class: "avatar s70 avatar-tile"
%h1.group-title
@#{@group.path}
= @group.name
%span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
= visibility_level_icon(@group.visibility_level, fw: false)
......
......@@ -18,5 +18,4 @@
- if current_user
To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page.
.prepend-top-default
= render 'shared/merge_requests'
......@@ -4,7 +4,7 @@
= icon('caret-down')
.dropdown-menu-nav.dropdown-menu-align-right
%ul
- if @group
- if @group&.persisted?
- create_group_project = can?(current_user, :create_projects, @group)
- create_group_subgroup = can?(current_user, :create_subgroup, @group)
- if create_group_project || create_group_subgroup
......@@ -18,7 +18,7 @@
%li.divider
%li.dropdown-bold-header GitLab
- if @project && @project.persisted?
- if @project&.persisted?
- create_project_issue = can?(current_user, :create_issue, @project)
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- create_project_snippet = can?(current_user, :create_project_snippet, @project)
......
= render 'profiles/head'
= form_for @user, url: profile_path, method: :put, html: { multipart: true, class: "edit-user prepend-top-default" }, authenticity_token: true do |f|
= bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user prepend-top-default' }, authenticity_token: true do |f|
= form_errors(@user)
.row
......@@ -11,11 +11,11 @@
- if @user.avatar?
You can change your avatar here
- if gravatar_enabled?
or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host}
or remove the current avatar to revert to #{link_to Gitlab.config.gravatar.host, 'http://' + Gitlab.config.gravatar.host}
- else
You can upload an avatar here
- if gravatar_enabled?
or change it at #{link_to Gitlab.config.gravatar.host, "http://" + Gitlab.config.gravatar.host}
or change it at #{link_to Gitlab.config.gravatar.host, 'http://' + Gitlab.config.gravatar.host}
.col-lg-9
.clearfix.avatar-image.append-bottom-default
= link_to avatar_icon(@user, 400), target: '_blank', rel: 'noopener noreferrer' do
......@@ -26,12 +26,12 @@
%a.btn.js-choose-user-avatar-button
Browse file...
%span.avatar-file-name.prepend-left-default.js-avatar-filename No file chosen
= f.file_field :avatar, class: "js-user-avatar-input hidden", accept: "image/*"
= f.file_field_without_bootstrap :avatar, class: 'js-user-avatar-input hidden', accept: 'image/*'
.help-block
The maximum file size allowed is 200KB.
- if @user.avatar?
%hr
= link_to 'Remove avatar', profile_avatar_path, data: { confirm: "Avatar will be removed. Are you sure?" }, method: :delete, class: "btn btn-gray"
= link_to 'Remove avatar', profile_avatar_path, data: { confirm: 'Avatar will be removed. Are you sure?' }, method: :delete, class: 'btn btn-gray'
%hr
.row
.col-lg-3.profile-settings-sidebar
......@@ -43,91 +43,50 @@
Some options are unavailable for LDAP accounts
.col-lg-9
.row
.form-group.col-md-9
= f.label :name, class: "label-light"
= f.text_field :name, class: "form-control", required: true
%span.help-block Enter your name, so people you know can recognize you.
= f.text_field :name, required: true, wrapper: { class: 'col-md-9' },
help: 'Enter your name, so people you know can recognize you.'
= f.text_field :id, readonly: true, label: 'User ID', wrapper: { class: 'col-md-3' }
.form-group.col-md-3
= f.label :id, class: 'label-light' do
User ID
= f.text_field :id, class: 'form-control', readonly: true
.form-group
= f.label :email, class: "label-light"
- if @user.external_email?
= f.text_field :email, class: "form-control", required: true, readonly: true
%span.help-block.light
Your email address was automatically set based on your #{email_provider_label} account.
- else
- if @user.temp_oauth_email?
= f.text_field :email, class: "form-control", required: true, value: nil
- else
= f.text_field :email, class: "form-control", required: true
- if @user.unconfirmed_email.present?
%span.help-block
Please click the link in the confirmation email before continuing. It was sent to
= succeed "." do
%strong= @user.unconfirmed_email
%p
= link_to "Resend confirmation e-mail", user_confirmation_path(user: { email: @user.unconfirmed_email }), method: :post
= f.text_field :email, required: true, readonly: true, help: "Your email address was automatically set based on your #{email_provider_label} account."
- else
%span.help-block We also use email for avatar detection if no avatar is uploaded.
.form-group
= f.label :public_email, class: "label-light"
= f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email), { include_blank: 'Do not show on profile' }, class: "select2"
%span.help-block This email will be displayed on your public profile.
.form-group
= f.label :preferred_language, class: "label-light"
= f.text_field :email, required: true, value: (@user.email unless @user.temp_oauth_email?),
help: user_email_help_text(@user)
= f.select :public_email, options_for_select(@user.all_emails, selected: @user.public_email),
{ help: 'This email will be displayed on your public profile.', include_blank: 'Do not show on profile' },
control_class: 'select2'
= f.select :preferred_language, Gitlab::I18n::AVAILABLE_LANGUAGES.map { |value, label| [label, value] },
{}, class: "select2"
%span.help-block This feature is experimental and translations are not complete yet.
.form-group
= f.label :skype, class: "label-light"
= f.text_field :skype, class: "form-control"
.form-group
= f.label :linkedin, class: "label-light"
= f.text_field :linkedin, class: "form-control"
.form-group
= f.label :twitter, class: "label-light"
= f.text_field :twitter, class: "form-control"
.form-group
= f.label :website_url, 'Website', class: "label-light"
= f.text_field :website_url, class: "form-control"
.form-group
= f.label :location, 'Location', class: "label-light"
= f.text_field :location, class: "form-control"
.form-group
= f.label :organization, 'Organization', class: "label-light"
= f.text_field :organization, class: "form-control"
.form-group
= f.label :bio, class: "label-light"
= f.text_area :bio, rows: 4, class: "form-control", maxlength: 250
%span.help-block Tell us about yourself in fewer than 250 characters.
{ help: 'This feature is experimental and translations are not complete yet.' },
control_class: 'select2'
= f.text_field :skype
= f.text_field :linkedin
= f.text_field :twitter
= f.text_field :website_url, label: 'Website'
= f.text_field :location
= f.text_field :organization
= f.text_area :bio, rows: 4, maxlength: 250, help: 'Tell us about yourself in fewer than 250 characters.'
.prepend-top-default.append-bottom-default
= f.submit 'Update profile settings', class: "btn btn-success"
= link_to "Cancel", user_path(current_user), class: "btn btn-cancel"
= f.submit 'Update profile settings', class: 'btn btn-success'
= link_to 'Cancel', user_path(current_user), class: 'btn btn-cancel'
.modal.modal-profile-crop
.modal-dialog
.modal-content
.modal-header
%button.close{ :type => "button", :'data-dismiss' => "modal" }
%button.close{ type: 'button', 'data-dismiss': 'modal' }
%span
&times;
%h4.modal-title
Position and size your new avatar
.modal-body
.profile-crop-image-container
%img.modal-profile-crop-image{ alt: "Avatar cropper" }
%img.modal-profile-crop-image{ alt: 'Avatar cropper' }
.crop-controls
.btn-group
%button.btn.btn-primary{ data: { method: "zoom", option: "0.1" } }
%button.btn.btn-primary{ data: { method: 'zoom', option: '0.1' } }
%span.fa.fa-search-plus
%button.btn.btn-primary{ data: { method: "zoom", option: "-0.1" } }
%button.btn.btn-primary{ data: { method: 'zoom', option: '-0.1' } }
%span.fa.fa-search-minus
.modal-footer
%button.btn.btn-primary.js-upload-user-avatar{ :type => "button" }
%button.btn.btn-primary.js-upload-user-avatar{ type: 'button' }
Set new profile picture
- @no_container = true
- container_class = !fluid_layout && diff_view == :inline ? 'container-limited' : ''
- limited_container_width = fluid_layout || diff_view == :inline ? '' : 'limit-container-width'
- limited_container_width = fluid_layout ? '' : 'limit-container-width'
- page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
- page_description @commit.description
= render "projects/commits/head"
......@@ -13,6 +13,7 @@
.block-connector
= render "projects/diffs/diffs", diffs: @diffs, environment: @environment
.limited-width-notes
= render "shared/notes/notes_with_form", :autocomplete => true
- if can_collaborate_with_project?
- %w(revert cherry-pick).each do |type|
......
......@@ -3,8 +3,8 @@
- commits.chunk { |c| c.committed_date.in_time_zone.to_date }.each do |day, commits|
%li.commit-header.js-commit-header{ data: { day: day } }
%span.day= day.strftime('%d %b, %Y')
%span.commits-count= pluralize(commits.count, 'commit')
%span.day= l(day, format: '%d %b, %Y')
%span.commits-count= n_("%d commit", "%d commits", commits.count) % commits.count
%li.commits-row{ data: { day: day } }
%ul.content-list.commit-list
......@@ -12,4 +12,4 @@
- if hidden > 0
%li.alert.alert-warning
#{number_with_delimiter(hidden)} additional commits have been omitted to prevent performance issues.
= n_('%d additional commit has been omitted to prevent performance issues.', '%d additional commits have been omitted to prevent performance issues.', hidden) % number_with_delimiter(hidden)
- @no_container = true
- page_title "Commits", @ref
- page_title _("Commits"), @ref
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits")
......@@ -18,16 +18,16 @@
.block-controls.hidden-xs.hidden-sm
- if @merge_request.present?
.control
= link_to "View open merge request", namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn'
= link_to _("View open merge request"), namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'btn'
- elsif create_mr_button?(@repository.root_ref, @ref)
.control
= link_to "Create merge request", create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success'
= link_to _("Create merge request"), create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success'
.control
= form_tag(namespace_project_commits_path(@project.namespace, @project, @id), method: :get, class: 'commits-search-form') do
= search_field_tag :search, params[:search], { placeholder: 'Filter by commit message', id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false }
= search_field_tag :search, params[:search], { placeholder: _('Filter by commit message'), id: 'commits-search', class: 'form-control search-text-input input-short', spellcheck: false }
.control
= link_to namespace_project_commits_path(@project.namespace, @project, @ref, rss_url_options), title: "Commits feed", class: 'btn' do
= link_to namespace_project_commits_path(@project.namespace, @project, @ref, rss_url_options), title: _("Commits feed"), class: 'btn' do
= icon("rss")
%div{ id: dom_id(@project) }
......
......@@ -4,7 +4,7 @@
%h4
Deploy Keys
%button.btn.js-settings-toggle
= expanded ? 'Close' : 'Expand'
= expanded ? 'Collapse' : 'Expand'
%p
Deploy keys allow read-only or read-write (if enabled) access to your repository. Deploy keys can be used for CI, staging or production servers. You can create a deploy key or add an existing one.
.settings-content.no-animate{ class: ('expanded' if expanded) }
......
......@@ -3,14 +3,15 @@
.table-mobile-header{ role: 'rowheader' } ID
%strong.table-mobile-content ##{deployment.iid}
.table-section.section-40{ role: 'gridcell' }
.table-section.section-30{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' } Commit
= render 'projects/deployments/commit', deployment: deployment
.table-section.section-15.build-column{ role: 'gridcell' }
.table-section.section-25.build-column{ role: 'gridcell' }
.table-mobile-header{ role: 'rowheader' } Job
- if deployment.deployable
= link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link table-mobile-content' do
.table-mobile-content
= link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link' do
#{deployment.deployable.name} (##{deployment.deployable.id})
- if deployment.user
by
......@@ -21,6 +22,6 @@
%span.table-mobile-content= time_ago_with_tooltip(deployment.created_at)
.table-section.section-20.table-button-footer{ role: 'gridcell' }
.btn-group.table-action-button
.btn-group.table-action-buttons
= render 'projects/deployments/actions', deployment: deployment
= render 'projects/deployments/rollback', deployment: deployment
- @content_class = "limit-container-width" unless fluid_layout
= render "projects/settings/head"
.project-edit-container
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
Project settings
.col-lg-9
.col-lg-8
.project-edit-errors
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit-project" }, authenticity_token: true do |f|
%fieldset
......@@ -39,66 +41,66 @@
Sharing &amp; Permissions
.form_group.prepend-top-20.sharing-and-permissions
.row.js-visibility-select
.col-md-9
.col-md-8
.label-light
= label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level
= link_to icon('question-circle'), help_page_path("public_access/public_access")
%span.help-block
.col-md-3.visibility-select-container
.col-md-4.visibility-select-container
= render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level)
= f.fields_for :project_feature do |feature_fields|
%fieldset.features
.row
.col-md-9.project-feature
.col-md-8.project-feature
= feature_fields.label :repository_access_level, "Repository", class: 'label-light'
%span.help-block View and edit files in this project
.col-md-3.js-repo-access-level
.col-md-4.js-repo-access-level
= project_feature_access_select(:repository_access_level)
.row
.col-md-9.project-feature.nested
.col-md-8.project-feature.nested
= feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light'
%span.help-block Submit changes to be merged upstream
.col-md-3
.col-md-4
= project_feature_access_select(:merge_requests_access_level)
.row
.col-md-9.project-feature.nested
.col-md-8.project-feature.nested
= feature_fields.label :builds_access_level, "Pipelines", class: 'label-light'
%span.help-block Build, test, and deploy your changes
.col-md-3
.col-md-4
= project_feature_access_select(:builds_access_level)
.row
.col-md-9.project-feature
.col-md-8.project-feature
= feature_fields.label :snippets_access_level, "Snippets", class: 'label-light'
%span.help-block Share code pastes with others out of Git repository
.col-md-3
.col-md-4
= project_feature_access_select(:snippets_access_level)
.row
.col-md-9.project-feature
.col-md-8.project-feature
= feature_fields.label :issues_access_level, "Issues", class: 'label-light'
%span.help-block Lightweight issue tracking system for this project
.col-md-3
.col-md-4
= project_feature_access_select(:issues_access_level)
.row
.col-md-9.project-feature
.col-md-8.project-feature
= feature_fields.label :wiki_access_level, "Wiki", class: 'label-light'
%span.help-block Pages for project documentation
.col-md-3
.col-md-4
= project_feature_access_select(:wiki_access_level)
.form-group
= render 'shared/allow_request_access', form: f
- if Gitlab.config.lfs.enabled && current_user.admin?
.row.js-lfs-enabled
.col-md-9
.col-md-8
= f.label :lfs_enabled, 'LFS', class: 'label-light'
%span.help-block
Git Large File Storage
= link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
.col-md-3
.col-md-4
.select-wrapper
= f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select select-control', data: { field: 'lfs_enabled' }
= icon('chevron-down')
......@@ -138,19 +140,19 @@
.row.prepend-top-default
%hr
.row.prepend-top-default
.col-lg-3
.col-lg-4
%h4.prepend-top-0
Housekeeping
%p.append-bottom-0
%p
Runs a number of housekeeping tasks within the current repository,
such as compressing file revisions and removing unreachable objects.
.col-lg-9
.col-lg-8
= link_to 'Housekeeping', housekeeping_namespace_project_path(@project.namespace, @project),
method: :post, class: "btn btn-default"
%hr
.row.prepend-top-default
.col-lg-3
.col-lg-4
%h4.prepend-top-0
Export project
%p.append-bottom-0
......@@ -159,7 +161,7 @@
%p
Once the exported file is ready, you will receive a notification email with a download link.
.col-lg-9
.col-lg-8
- if @project.export_project_path
= link_to 'Download export', download_export_namespace_project_path(@project.namespace, @project),
......@@ -190,7 +192,7 @@
- if can? current_user, :archive_project, @project
%hr
.row.prepend-top-default
.col-lg-3
.col-lg-4
%h4.warning-title.prepend-top-0
- if @project.archived?
Unarchive project
......@@ -201,7 +203,7 @@
Unarchiving the project will mark its repository as active. The project can be committed to.
- else
Archiving the project will mark its repository as read-only. It is hidden from the dashboard and doesn't show up in searches.
.col-lg-9
.col-lg-8
- if @project.archived?
%p
%strong Once active this project shows up in the search and on the dashboard.
......@@ -216,10 +218,10 @@
method: :post, class: "btn btn-warning"
%hr
.row.prepend-top-default
.col-lg-3
.col-lg-4
%h4.prepend-top-0.warning-title
Rename repository
.col-lg-9
.col-lg-8
= render 'projects/errors'
= form_for([@project.namespace.becomes(Namespace), @project]) do |f|
.form-group.project_name_holder
......@@ -244,12 +246,12 @@
- if can?(current_user, :change_namespace, @project)
%hr
.row.prepend-top-default
.col-lg-3
.col-lg-4
%h4.prepend-top-0.danger-title
Transfer project to new group
%p.append-bottom-0
Please select the group you want to transfer this project to in the dropdown to the right.
.col-lg-9
.col-lg-8
= form_for([@project.namespace.becomes(Namespace), @project], url: transfer_namespace_project_path(@project.namespace, @project), method: :put, remote: true, html: { class: 'js-project-transfer-form' } ) do |f|
.form-group
= label_tag :new_namespace_id, nil, class: 'label-light' do
......@@ -265,7 +267,7 @@
- if @project.forked? && can?(current_user, :remove_fork_project, @project)
%hr
.row.prepend-top-default.append-bottom-default
.col-lg-3
.col-lg-4
%h4.prepend-top-0.danger-title
Remove fork relationship
%p.append-bottom-0
......@@ -273,7 +275,7 @@
This will remove the fork relationship to source project
= succeed "." do
= link_to @project.forked_from_project.name_with_namespace, project_path(@project.forked_from_project)
.col-lg-9
.col-lg-8
= form_for([@project.namespace.becomes(Namespace), @project], url: remove_fork_namespace_project_path(@project.namespace, @project), method: :delete, remote: true, html: { class: 'transfer-project' }) do |f|
%p
%strong Once removed, the fork relationship cannot be restored and you will no longer be able to send merge requests to the source.
......@@ -281,12 +283,12 @@
- if can?(current_user, :remove_project, @project)
%hr
.row.prepend-top-default.append-bottom-default
.col-lg-3
.col-lg-4
%h4.prepend-top-0.danger-title
Remove project
%p.append-bottom-0
Removing the project will delete its repository and all related resources including issues, merge requests etc.
.col-lg-9
.col-lg-8
= form_tag(namespace_project_path(@project.namespace, @project), method: :delete) do
%p
%strong Removed projects cannot be restored!
......
.row.prepend-top-default
.col-lg-3
.col-lg-4
%h4.prepend-top-0
= page_title
%p
#{link_to 'Webhooks', help_page_path('user/project/integrations/webhooks')} can be
used for binding events when something is happening within the project.
.col-lg-9.append-bottom-default
.col-lg-8.append-bottom-default
= form_for @hook, as: :hook, url: polymorphic_path([@project.namespace.becomes(Namespace), @project, :hooks]) do |f|
= render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook }
= f.submit 'Add webhook', class: 'btn btn-create'
......
......@@ -15,7 +15,7 @@
.col-md-6
= render 'projects/pipelines/charts/overall'
.col-md-6
= render 'projects/pipelines/charts/build_times'
= render 'projects/pipelines/charts/pipeline_times'
%hr
= render 'projects/pipelines/charts/builds'
= render 'projects/pipelines/charts/pipelines'
......@@ -2,18 +2,14 @@
%ul
%li
Total:
%strong= pluralize @project.builds.count(:all), 'job'
%strong= pluralize @counts[:total], 'pipeline'
%li
Successful:
%strong= pluralize @project.builds.success.count(:all), 'job'
%strong= pluralize @counts[:success], 'pipeline'
%li
Failed:
%strong= pluralize @project.builds.failed.count(:all), 'job'
%strong= pluralize @counts[:failed], 'pipeline'
%li
Success ratio:
%strong
#{success_ratio(@project.builds.success, @project.builds.failed)}%
%li
Commits covered:
%strong
= @project.pipelines.count(:all)
#{success_ratio(@counts)}%
......@@ -6,7 +6,7 @@
:javascript
var data = {
labels : #{@charts[:build_times].labels.to_json},
labels : #{@charts[:pipeline_times].labels.to_json},
datasets : [
{
fillColor : "rgba(220,220,220,0.5)",
......@@ -14,7 +14,7 @@
barStrokeWidth: 1,
barValueSpacing: 1,
barDatasetSpacing: 1,
data : #{@charts[:build_times].build_times.to_json}
data : #{@charts[:pipeline_times].pipeline_times.to_json}
}
]
}
......
%div{ class: badge.title.gsub(' ', '-') }
.col-lg-3.profile-settings-sidebar
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
= badge.title.capitalize
.col-lg-9
.col-lg-8
.prepend-top-10
.panel.panel-default
.panel-heading
......
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
Pipelines
.col-lg-9
.col-lg-8
= form_for @project, url: namespace_project_pipelines_settings_path(@project.namespace.becomes(Namespace), @project) do |f|
%fieldset.builds-feature
- unless @repository.gitlab_ci_yml
......
.row.prepend-top-default
.col-lg-3.settings-sidebar
.col-lg-4.settings-sidebar
%h4.prepend-top-0
Project members
- if can?(current_user, :admin_project_member, @project)
......@@ -13,7 +13,7 @@
%i Masters
or
%i Owners
.col-lg-9
.col-lg-8
.light
- if can?(current_user, :admin_project_member, @project)
%ul.nav-links.project-member-tabs{ role: 'tablist' }
......
......@@ -7,7 +7,7 @@
%h4
Protected Branches
%button.btn.js-settings-toggle
= expanded ? 'Close' : 'Expand'
= expanded ? 'Collapse' : 'Expand'
%p
Keep stable branches secure and force developers to use merge requests.
.settings-content.no-animate{ class: ('expanded' if expanded) }
......
......@@ -7,7 +7,7 @@
%h4
Protected Tags
%button.btn.js-settings-toggle
= expanded ? 'Close' : 'Expand'
= expanded ? 'Collapse' : 'Expand'
%p
Limit access to creating and updating tags.
.settings-content.no-animate{ class: ('expanded' if expanded) }
......
......@@ -23,3 +23,7 @@
- disabled_title = @service.disabled_title
= link_to 'Cancel', namespace_project_settings_integrations_path(@project.namespace, @project), class: 'btn btn-cancel'
- if lookup_context.template_exists?('show', "projects/services/#{@service.to_param}", true)
%hr
= render "projects/services/#{@service.to_param}/show"
.row.prepend-top-default.append-bottom-default
.col-lg-3
.col-lg-4
%h4.prepend-top-0
Project services
%p Project services allow you to integrate GitLab with other applications
.col-lg-9
.col-lg-8
%table.table
%colgroup
%col
......
- content_for :page_specific_javascripts do
= webpack_bundle_tag('prometheus_metrics')
.row.prepend-top-default.append-bottom-default.prometheus-metrics-monitoring.js-prometheus-metrics-monitoring
.col-lg-3
%h4.prepend-top-0
Metrics
%p
Metrics are automatically configured and monitored
based on a library of metrics from popular exporters.
= link_to 'More information', '#'
.col-lg-9
.panel.panel-default.js-panel-monitored-metrics{ data: { "active-metrics" => "#{namespace_project_prometheus_active_metrics_path(@project.namespace, @project, :json)}" } }
.panel-heading
%h3.panel-title
Monitored
%span.badge.js-monitored-count 0
.panel-body
.loading-metrics.text-center.js-loading-metrics
= icon('spinner spin 3x', class: 'metrics-load-spinner')
%p Finding and configuring metrics...
.empty-metrics.text-center.hidden.js-empty-metrics
= custom_icon('icon_empty_metrics')
%p No metrics are being monitored. To start monitoring, deploy to an environment.
= link_to project_environments_path(@project), title: 'View environments', class: 'btn btn-success' do
View environments
%ul.list-unstyled.metrics-list.hidden.js-metrics-list
.panel.panel-default.hidden.js-panel-missing-env-vars
.panel-heading
%h3.panel-title
= icon('caret-right lg fw', class: 'panel-toggle js-panel-toggle', 'aria-label' => 'Toggle panel')
Missing environment variable
%span.badge.js-env-var-count 0
.panel-body.hidden
.flash-container
.flash-notice
.flash-text
To set up automatic monitoring, add the environment variable
%code
$CI_ENVIRONMENT_SLUG
to exporter&rsquo;s queries.
= link_to 'More information', '#'
%ul.list-unstyled.metrics-list.js-missing-var-metrics-list
- @content_class = "limit-container-width" unless fluid_layout
- page_title "Pipelines"
= render "projects/settings/head"
......
- @content_class = "limit-container-width" unless fluid_layout
- page_title 'Integrations'
= render "projects/settings/head"
= render 'projects/hooks/index'
......
- @content_class = "limit-container-width" unless fluid_layout
- page_title "Members"
= render "projects/settings/head"
......
- @content_class = "limit-container-width limited-inner-width-container" unless fluid_layout
- page_title "#{@snippet.title} (#{@snippet.to_reference})", "Snippets"
= render 'shared/snippets/header'
......@@ -9,4 +10,4 @@
.row-content-block.top-block.content-component-block
= render 'award_emoji/awards_block', awardable: @snippet, inline: true
#notes= render "shared/notes/notes_with_form", :autocomplete => true
#notes.limited-width-notes= render "shared/notes/notes_with_form", :autocomplete => true
- if readme.rich_viewer
%article.file-holder.readme-holder
%article.file-holder.readme-holder{ class: ("limited-width-container" unless fluid_layout) }
.js-file-title.file-title
= blob_icon readme.mode, readme.name
= link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@ref, readme.path)) do
......
.row.prepend-top-default.append-bottom-default.triggers-container
.col-lg-3
.col-lg-4
= render "projects/triggers/content"
.col-lg-9
.col-lg-8
.panel.panel-default
.panel-heading
%h4.panel-title
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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