Commit 7a6e8826 authored by Mark Fletcher's avatar Mark Fletcher

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

parents a674e131 bbbc815a
......@@ -69,6 +69,10 @@ gem 'net-ldap'
# Git Wiki
# Required manually in config/initializers/gollum.rb to control load order
# Before updating this gem, check if
# https://github.com/gollum/gollum-lib/pull/292 has been merged.
# If it has, then remove the monkey patch for update_page, rename_page and raw_data_in_committer
# in config/initializers/gollum.rb
gem 'gollum-lib', '~> 4.2', require: false
# Before updating this gem, check if
......@@ -407,6 +411,8 @@ end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.83.0', require: 'gitaly'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
gem 'google-protobuf', '= 3.5.1'
gem 'toml-rb', '~> 0.3.15', require: false
......
......@@ -340,7 +340,7 @@ GEM
mime-types (~> 3.0)
representable (~> 3.0)
retriable (>= 2.0, < 4.0)
google-protobuf (3.5.1.1)
google-protobuf (3.5.1)
googleapis-common-protos-types (1.0.1)
google-protobuf (~> 3.0)
googleauth (0.5.3)
......@@ -1066,6 +1066,7 @@ DEPENDENCIES
gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0)
google-api-client (~> 0.13.6)
google-protobuf (= 3.5.1)
gpgme
grape (~> 1.0)
grape-entity (~> 0.6.0)
......
{"iconCount":189,"spriteSize":85900,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-down","arrow-right","assignee","bold","book","bookmark","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder-o","folder-open","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","image-comment-light","import","issue-block","issue-child","issue-close","issue-duplicate","issue-external","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil-square","pencil","pipeline","play","plus-square-o","plus-square","plus","podcast","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","spam","spinner","staged","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_notfound","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","unstaged","user","users","volume-up","warning","work"]}
\ No newline at end of file
{"iconCount":191,"spriteSize":86607,"icons":["abuse","account","admin","angle-double-left","angle-double-right","angle-down","angle-left","angle-right","angle-up","appearance","applications","approval","arrow-down","arrow-right","assignee","bold","book","bookmark","branch","bullhorn","calendar","cancel","chart","chevron-down","chevron-left","chevron-right","chevron-up","clock","close","code","collapse","comment-dots","comment-next","comment","comments","commit","credit-card","cut","dashboard","disk","doc_code","doc_image","doc_text","double-headed-arrow","download","duplicate","earth","ellipsis_v","emoji_slightly_smiling_face","emoji_smile","emoji_smiley","epic","external-link","eye-slash","eye","file-addition","file-deletion","file-modified","filter","folder-o","folder-open","folder","fork","geo-nodes","git-merge","group","history","home","hook","hourglass","image-comment-dark","image-comment-light","import","issue-block","issue-child","issue-close","issue-duplicate","issue-external","issue-new","issue-open-m","issue-open","issue-parent","issues","italic","key-2","key","label","labels","leave","level-up","license","link","list-bulleted","list-numbered","location-dot","location","lock-open","lock","log","mail","menu","merge-request-close","messages","mobile-issue-close","monitor","more","notifications-off","notifications","overview","pencil-square","pencil","pipeline","play","plus-square-o","plus-square","plus","podcast","preferences","profile","project","push-rules","question-o","question","quote","redo","remove","repeat","retry","scale","screen-full","screen-normal","scroll_down","scroll_up","search","settings","shield","slight-frown","slight-smile","smile","smiley","snippet","soft-unwrap","soft-wrap","spam","spinner","staged","star-o","star","status_canceled_borderless","status_canceled","status_closed","status_created_borderless","status_created","status_failed_borderless","status_failed","status_manual_borderless","status_manual","status_notfound_borderless","status_notfound","status_open","status_pending_borderless","status_pending","status_running_borderless","status_running","status_skipped_borderless","status_skipped","status_success_borderless","status_success_solid","status_success","status_warning_borderless","status_warning","stop","task-done","template","terminal","thumb-down","thumb-up","thumbtack","timer","todo-add","todo-done","token","unapproval","unassignee","unlink","unstaged","user","users","volume-up","warning","work"]}
\ No newline at end of file
This source diff could not be displayed because it is too large. You can view the blob instead.
<svg xmlns="http://www.w3.org/2000/svg" width="142" height="104" viewBox="0 0 142 104"><g fill="none" fill-rule="evenodd"><g transform="translate(112 4)"><path fill="#FFF" d="M8 4a4 4 0 0 0-4 4v14a4 4 0 0 0 4 4h14a4 4 0 0 0 4-4V8a4 4 0 0 0-4-4H8z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M8 4a4 4 0 0 0-4 4v14a4 4 0 0 0 4 4h14a4 4 0 0 0 4-4V8a4 4 0 0 0-4-4H8zm0-4h14a8 8 0 0 1 8 8v14a8 8 0 0 1-8 8H8a8 8 0 0 1-8-8V8a8 8 0 0 1 8-8z"/><rect width="10" height="10" x="10" y="10" fill="#FC6D26" rx="5"/></g><g transform="translate(5 74)"><rect width="30" height="30" fill="#FFF" rx="8"/><path fill="#E1DBF1" fill-rule="nonzero" d="M8 4a4 4 0 0 0-4 4v14a4 4 0 0 0 4 4h14a4 4 0 0 0 4-4V8a4 4 0 0 0-4-4H8zm0-4h14a8 8 0 0 1 8 8v14a8 8 0 0 1-8 8H8a8 8 0 0 1-8-8V8a8 8 0 0 1 8-8z"/><rect width="10" height="10" x="10" y="10" fill="#6B4FBB" rx="5"/></g><path fill="#FFF" d="M6 4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H6z"/><path fill="#FDC4A8" fill-rule="nonzero" d="M6 4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H6zm0-4h12a6 6 0 0 1 6 6v12a6 6 0 0 1-6 6H6a6 6 0 0 1-6-6V6a6 6 0 0 1 6-6z"/><rect width="8" height="8" x="8" y="8" fill="#FC6D26" rx="4"/><g transform="translate(112 77)"><rect width="24" height="24" fill="#FFF" rx="6"/><path fill="#E1DBF1" fill-rule="nonzero" d="M6 4a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2H6zm0-4h12a6 6 0 0 1 6 6v12a6 6 0 0 1-6 6H6a6 6 0 0 1-6-6V6a6 6 0 0 1 6-6z"/><rect width="8" height="8" x="8" y="8" fill="#6B4FBB" rx="4"/></g><g transform="translate(46 29)"><rect width="46" height="46" y="2" fill="#E1DBF1" rx="10"/><rect width="46" height="46" fill="#E1DBF1" rx="10"/><path fill="#C3B8E3" fill-rule="nonzero" d="M10 4a6 6 0 0 0-6 6v26a6 6 0 0 0 6 6h26a6 6 0 0 0 6-6V10a6 6 0 0 0-6-6H10zm0-4h26c5.523 0 10 4.477 10 10v26c0 5.523-4.477 10-10 10H10C4.477 46 0 41.523 0 36V10C0 4.477 4.477 0 10 0z"/><rect width="14" height="14" x="16" y="16" fill="#6B4FBB" rx="2"/></g><path fill="#E1DBF1" fill-rule="nonzero" d="M98.413 35.682a2 2 0 1 1-2.826-2.83l2.122-2.12a2 2 0 1 1 2.827 2.83l-2.123 2.12z"/><path fill="#C3B8E3" d="M104.78 29.32a2 2 0 0 1-2.826-2.829l2.122-2.12a2 2 0 0 1 2.827 2.83l-2.122 2.12z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M42.413 89.682a2 2 0 1 1-2.826-2.83l2.122-2.12a2 2 0 1 1 2.827 2.83l-2.123 2.12z"/><path fill="#E1DBF1" d="M48.78 83.32a2 2 0 1 1-2.826-2.829l2.122-2.12a2 2 0 1 1 2.827 2.83l-2.122 2.12z"/><path fill="#E1DBF1" fill-rule="nonzero" d="M27.713 26.531a2 2 0 1 1 2.574-3.062l2.296 1.93a2 2 0 1 1-2.573 3.062l-2.297-1.93z"/><path fill="#C3B8E3" d="M34.604 32.321a2 2 0 1 1 2.573-3.062l2.297 1.93A2 2 0 0 1 36.9 34.25l-2.297-1.93z"/><path fill="#C3B8E3" fill-rule="nonzero" d="M93.74 74.553a2 2 0 0 1 2.52-3.106l2.33 1.891a2 2 0 1 1-2.521 3.106l-2.33-1.891z"/><path fill="#E1DBF1" d="M100.727 80.225a2 2 0 1 1 2.521-3.105l2.33 1.89a2 2 0 1 1-2.522 3.106l-2.33-1.89z"/></g></svg>
\ No newline at end of file
......@@ -2,7 +2,7 @@
import Sortable from 'vendor/Sortable';
import Vue from 'vue';
import AccessorUtilities from '../../lib/utils/accessor';
import boardList from './board_list';
import boardList from './board_list.vue';
import boardBlankState from './board_blank_state';
import './board_delete';
......
<script>
import Sortable from 'vendor/Sortable';
import boardNewIssue from './board_new_issue';
import boardCard from './board_card.vue';
......@@ -8,6 +9,11 @@ const Store = gl.issueBoards.BoardsStore;
export default {
name: 'BoardList',
components: {
boardCard,
boardNewIssue,
loadingIcon,
},
props: {
disabled: {
type: Boolean,
......@@ -42,46 +48,6 @@ export default {
showIssueForm: false,
};
},
components: {
boardCard,
boardNewIssue,
loadingIcon,
},
methods: {
listHeight() {
return this.$refs.list.getBoundingClientRect().height;
},
scrollHeight() {
return this.$refs.list.scrollHeight;
},
scrollTop() {
return this.$refs.list.scrollTop + this.listHeight();
},
scrollToTop() {
this.$refs.list.scrollTop = 0;
},
loadNextPage() {
const getIssues = this.list.nextPage();
const loadingDone = () => {
this.list.loadingMore = false;
};
if (getIssues) {
this.list.loadingMore = true;
getIssues
.then(loadingDone)
.catch(loadingDone);
}
},
toggleForm() {
this.showIssueForm = !this.showIssueForm;
},
onScroll() {
if (!this.loadingMore && (this.scrollTop() > this.scrollHeight() - this.scrollOffset)) {
this.loadNextPage();
}
},
},
watch: {
filters: {
handler() {
......@@ -157,51 +123,90 @@ export default {
eventHub.$off(`scroll-board-list-${this.list.id}`, this.scrollToTop);
this.$refs.list.removeEventListener('scroll', this.onScroll);
},
template: `
<div class="board-list-component">
<div
class="board-list-loading text-center"
aria-label="Loading issues"
v-if="loading">
<loading-icon />
</div>
<board-new-issue
:list="list"
v-if="list.type !== 'closed' && showIssueForm"/>
<ul
class="board-list"
v-show="!loading"
ref="list"
:data-board="list.id"
:class="{ 'is-smaller': showIssueForm }">
<board-card
v-for="(issue, index) in issues"
ref="issue"
:index="index"
:list="list"
:issue="issue"
:issue-link-base="issueLinkBase"
:root-path="rootPath"
:disabled="disabled"
:key="issue.id" />
<li
class="board-list-count text-center"
v-if="showCount"
data-issue-id="-1">
methods: {
listHeight() {
return this.$refs.list.getBoundingClientRect().height;
},
scrollHeight() {
return this.$refs.list.scrollHeight;
},
scrollTop() {
return this.$refs.list.scrollTop + this.listHeight();
},
scrollToTop() {
this.$refs.list.scrollTop = 0;
},
loadNextPage() {
const getIssues = this.list.nextPage();
const loadingDone = () => {
this.list.loadingMore = false;
};
<loading-icon
v-show="list.loadingMore"
label="Loading more issues"
/>
if (getIssues) {
this.list.loadingMore = true;
getIssues
.then(loadingDone)
.catch(loadingDone);
}
},
toggleForm() {
this.showIssueForm = !this.showIssueForm;
},
onScroll() {
if (!this.loadingMore && (this.scrollTop() > this.scrollHeight() - this.scrollOffset)) {
this.loadNextPage();
}
},
},
};
</script>
<span v-if="list.issues.length === list.issuesSize">
Showing all issues
</span>
<span v-else>
Showing {{ list.issues.length }} of {{ list.issuesSize }} issues
</span>
</li>
</ul>
<template>
<div class="board-list-component">
<div
class="board-list-loading text-center"
aria-label="Loading issues"
v-if="loading">
<loading-icon />
</div>
`,
};
<board-new-issue
:list="list"
v-if="list.type !== 'closed' && showIssueForm"/>
<ul
class="board-list"
v-show="!loading"
ref="list"
:data-board="list.id"
:class="{ 'is-smaller': showIssueForm }">
<board-card
v-for="(issue, index) in issues"
ref="issue"
:index="index"
:list="list"
:issue="issue"
:issue-link-base="issueLinkBase"
:root-path="rootPath"
:disabled="disabled"
:key="issue.id" />
<li
class="board-list-count text-center"
v-if="showCount"
data-issue-id="-1">
<loading-icon
v-show="list.loadingMore"
label="Loading more issues"
/>
<span
v-if="list.issues.length === list.issuesSize"
>
Showing all issues
</span>
<span
v-else
>
Showing {{ list.issues.length }} of {{ list.issuesSize }} issues
</span>
</li>
</ul>
</div>
</template>
......@@ -35,10 +35,11 @@ export default class Clusters {
clusterStatus,
clusterStatusReason,
helpPath,
ingressHelpPath,
} = document.querySelector('.js-edit-cluster-form').dataset;
this.store = new ClustersStore();
this.store.setHelpPath(helpPath);
this.store.setHelpPaths(helpPath, ingressHelpPath);
this.store.updateStatus(clusterStatus);
this.store.updateStatusReason(clusterStatusReason);
this.service = new ClustersService({
......@@ -93,6 +94,7 @@ export default class Clusters {
props: {
applications: this.state.applications,
helpPath: this.state.helpPath,
ingressHelpPath: this.state.ingressHelpPath,
},
});
},
......@@ -172,7 +174,7 @@ export default class Clusters {
.map(appId => newApplicationMap[appId].title);
if (appTitles.length > 0) {
const text = sprintf(s__('ClusterIntegration|%{appList} was successfully installed on your cluster'), {
const text = sprintf(s__('ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster'), {
appList: appTitles.join(', '),
});
Flash(text, 'notice', this.successApplicationContainer);
......
......@@ -18,11 +18,16 @@
required: false,
default: '',
},
ingressHelpPath: {
type: String,
required: false,
default: '',
},
},
computed: {
generalApplicationDescription() {
return sprintf(
_.escape(s__(`ClusterIntegration|Install applications on your cluster.
_.escape(s__(`ClusterIntegration|Install applications on your Kubernetes cluster.
Read more about %{helpLink}`)),
{
helpLink: `<a href="${this.helpPath}">
......@@ -34,7 +39,7 @@
},
helmTillerDescription() {
return _.escape(s__(
`ClusterIntegration|Helm streamlines installing and managing Kubernets applications.
`ClusterIntegration|Helm streamlines installing and managing Kubernetes applications.
Tiller runs inside of your Kubernetes Cluster, and manages
releases of your charts.`,
));
......@@ -49,7 +54,7 @@
_.escape(s__(
`ClusterIntegration|%{boldNotice} This will add some extra resources
like a load balancer, which may incur additional costs depending on
the hosting provider Kubernetes is installed on. If you are using GKE,
the hosting provider your Kubernetes cluster is installed on. If you are using GKE,
you can %{pricingLink}.`,
)), {
boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
......@@ -59,13 +64,28 @@
false,
);
const externalIpParagraph = sprintf(
_.escape(s__(
`ClusterIntegration|After installing Ingress, you will need to point your wildcard DNS
at the generated external IP address in order to view your app after it is deployed. %{ingressHelpLink}`,
)), {
ingressHelpLink: `<a href="${this.ingressHelpPath}">
${_.escape(s__('ClusterIntegration|More information'))}
</a>`,
},
false,
);
return `
<p>
${descriptionParagraph}
</p>
<p class="append-bottom-0">
<p>
${extraCostParagraph}
</p>
<p class="settings-message append-bottom-0">
${externalIpParagraph}
</p>
`;
},
gitlabRunnerDescription() {
......
......@@ -4,6 +4,7 @@ export default class ClusterStore {
constructor() {
this.state = {
helpPath: null,
ingressHelpPath: null,
status: null,
statusReason: null,
applications: {
......@@ -39,8 +40,9 @@ export default class ClusterStore {
};
}
setHelpPath(helpPath) {
setHelpPaths(helpPath, ingressHelpPath) {
this.state.helpPath = helpPath;
this.state.ingressHelpPath = ingressHelpPath;
}
updateStatus(status) {
......
import _ from 'underscore';
import {
getSelector,
togglePopover,
inserted,
mouseenter,
mouseleave,
} from './feature_highlight_helper';
export function setupFeatureHighlightPopover(id, debounceTimeout = 300) {
const $selector = $(getSelector(id));
const $parent = $selector.parent();
const $popoverContent = $parent.siblings('.feature-highlight-popover-content');
const hideOnScroll = togglePopover.bind($selector, false);
const debouncedMouseleave = _.debounce(mouseleave, debounceTimeout);
$selector
// Setup popover
.data('content', $popoverContent.prop('outerHTML'))
.popover({
html: true,
// Override the existing template to add custom CSS classes
template: `
<div class="popover feature-highlight-popover" role="tooltip">
<div class="arrow"></div>
<div class="popover-content"></div>
</div>
`,
})
.on('mouseenter', mouseenter)
.on('mouseleave', debouncedMouseleave)
.on('inserted.bs.popover', inserted)
.on('show.bs.popover', () => {
window.addEventListener('scroll', hideOnScroll);
})
.on('hide.bs.popover', () => {
window.removeEventListener('scroll', hideOnScroll);
})
// Display feature highlight
.removeAttr('disabled');
}
export function findHighestPriorityFeature() {
let priorityFeature;
const sortedFeatureEls = [].slice.call(document.querySelectorAll('.js-feature-highlight')).sort((a, b) =>
(a.dataset.highlightPriority || 0) < (b.dataset.highlightPriority || 0));
const [priorityFeatureEl] = sortedFeatureEls;
if (priorityFeatureEl) {
priorityFeature = priorityFeatureEl.dataset.highlight;
}
return priorityFeature;
}
export function highlightFeatures() {
const priorityFeature = findHighestPriorityFeature();
if (priorityFeature) {
setupFeatureHighlightPopover(priorityFeature);
}
return priorityFeature;
}
import axios from '../lib/utils/axios_utils';
import { __ } from '../locale';
import Flash from '../flash';
import LazyLoader from '../lazy_loader';
export const getSelector = highlightId => `.js-feature-highlight[data-highlight=${highlightId}]`;
export function togglePopover(show) {
const isAlreadyShown = this.hasClass('js-popover-show');
if ((show && isAlreadyShown) || (!show && !isAlreadyShown)) {
return false;
}
this.popover(show ? 'show' : 'hide');
this.toggleClass('disable-animation js-popover-show', show);
return true;
}
export function dismiss(highlightId) {
axios.post(this.attr('data-dismiss-endpoint'), {
feature_name: highlightId,
})
.catch(() => Flash(__('An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again.')));
togglePopover.call(this, false);
this.hide();
}
export function mouseleave() {
if (!$('.popover:hover').length > 0) {
const $featureHighlight = $(this);
togglePopover.call($featureHighlight, false);
}
}
export function mouseenter() {
const $featureHighlight = $(this);
const showedPopover = togglePopover.call($featureHighlight, true);
if (showedPopover) {
$('.popover')
.on('mouseleave', mouseleave.bind($featureHighlight));
}
}
export function inserted() {
const popoverId = this.getAttribute('aria-describedby');
const highlightId = this.dataset.highlight;
const $popover = $(this);
const dismissWrapper = dismiss.bind($popover, highlightId);
$(`#${popoverId} .dismiss-feature-highlight`)
.on('click', dismissWrapper);
const lazyImg = $(`#${popoverId} .feature-highlight-illustration`)[0];
if (lazyImg) {
LazyLoader.loadImage(lazyImg);
}
}
import { highlightFeatures } from './feature_highlight';
import bp from '../breakpoints';
export default function domContentLoaded() {
if (bp.getBreakpointSize() === 'lg') {
highlightFeatures();
return true;
}
return false;
}
document.addEventListener('DOMContentLoaded', domContentLoaded);
export default class TransferDropdown {
constructor() {
this.groupDropdown = $('.js-groups-dropdown');
this.parentInput = $('#new_parent_group_id');
this.data = this.groupDropdown.data('data');
this.init();
}
init() {
this.buildDropdown();
}
buildDropdown() {
const extraOptions = [{ id: '', text: 'No parent group' }, 'divider'];
this.groupDropdown.glDropdown({
selectable: true,
filterable: true,
toggleLabel: item => item.text,
search: { fields: ['text'] },
data: extraOptions.concat(this.data),
text: item => item.text,
clicked: (options) => {
const { e } = options;
e.preventDefault();
this.assignSelected(options.selectedObj);
},
});
}
assignSelected(selected) {
this.parentInput.val(selected.id);
}
}
......@@ -26,6 +26,7 @@ import './gl_dropdown';
import initTodoToggle from './header';
import initImporterStatus from './importer_status';
import initLayoutNav from './layout_nav';
import './feature_highlight/feature_highlight_options';
import LazyLoader from './lazy_loader';
import initLogoAnimation from './logo';
import './milestone_select';
......
import _ from 'underscore';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { __ } from '~/locale';
export default function initBroadcastMessagesForm() {
$('input#broadcast_message_color').on('input', function onMessageColorInput() {
......@@ -18,13 +21,15 @@ export default function initBroadcastMessagesForm() {
if (message === '') {
$('.js-broadcast-message-preview').text('Your message here');
} else {
$.ajax({
url: previewPath,
type: 'POST',
data: {
broadcast_message: { message },
axios.post(previewPath, {
broadcast_message: {
message,
},
});
})
.then(({ data }) => {
$('.js-broadcast-message-preview').html(data.message);
})
.catch(() => flash(__('An error occurred while rendering preview broadcast message')));
}
}, 250));
}
import groupAvatar from '~/group_avatar';
import TransferDropdown from '~/groups/transfer_dropdown';
export default groupAvatar;
export default () => {
groupAvatar();
new TransferDropdown(); // eslint-disable-line no-new
};
/* eslint-disable comma-dangle, consistent-return, class-methods-use-this, arrow-parens, no-param-reassign, max-len */
import _ from 'underscore';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { __ } from '~/locale';
const debounceTimeoutDuration = 1000;
const invalidInputClass = 'gl-field-error-outline';
......@@ -77,12 +80,9 @@ export default class UsernameValidator {
this.state.pending = true;
this.state.available = false;
this.renderState();
return $.ajax({
type: 'GET',
url: `${gon.relative_url_root}/users/${username}/exists`,
dataType: 'json',
success: (res) => this.setAvailabilityState(res.exists)
});
axios.get(`${gon.relative_url_root}/users/${username}/exists`)
.then(({ data }) => this.setAvailabilityState(data.exists))
.catch(() => flash(__('An error occurred while validating username')));
}
}
......
......@@ -7,6 +7,10 @@
// more than `x` users are referenced.
//
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { __ } from '~/locale';
var lastTextareaPreviewed;
var lastTextareaHeight = null;
var markdownPreview;
......@@ -62,21 +66,17 @@ MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) {
success(this.ajaxCache.response);
return;
}
$.ajax({
type: 'POST',
url: url,
data: {
text: text
},
dataType: 'json',
success: (function (response) {
this.ajaxCache = {
text: text,
response: response
};
success(response);
}).bind(this)
});
axios.post(url, {
text,
})
.then(({ data }) => {
this.ajaxCache = {
text: text,
response: data,
};
success(data);
})
.catch(() => flash(__('An error occurred while fetching markdown preview')));
};
MarkdownPreview.prototype.hideReferencedUsers = function ($form) {
......
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import flash from './flash';
export default class ProjectLabelSubscription {
constructor(container) {
this.$container = $(container);
......@@ -17,10 +21,7 @@ export default class ProjectLabelSubscription {
$btn.addClass('disabled');
$span.toggleClass('hidden');
$.ajax({
type: 'POST',
url,
}).done(() => {
axios.post(url).then(() => {
let newStatus;
let newAction;
......@@ -45,6 +46,6 @@ export default class ProjectLabelSubscription {
return button;
});
});
}).catch(() => flash(__('There was an error subscribing to this label.')));
}
}
import axios from '../lib/utils/axios_utils';
import PANEL_STATE from './constants';
import { backOff } from '../lib/utils/common_utils';
......@@ -81,24 +82,20 @@ export default class PrometheusMetrics {
loadActiveMetrics() {
this.showMonitoringMetricsPanelState(PANEL_STATE.LOADING);
backOff((next, stop) => {
$.ajax({
url: this.activeMetricsEndpoint,
dataType: 'json',
global: false,
})
.done((res) => {
if (res && res.success) {
stop(res);
axios.get(this.activeMetricsEndpoint)
.then(({ data }) => {
if (data && data.success) {
stop(data);
} else {
this.backOffRequestCounter = this.backOffRequestCounter += 1;
if (this.backOffRequestCounter < 3) {
next();
} else {
stop(res);
stop(data);
}
}
})
.fail(stop);
.catch(stop);
})
.then((res) => {
if (res && res.data && res.data.length) {
......
/* eslint-disable no-new */
import Flash from '../flash';
import flash from '../flash';
import axios from '../lib/utils/axios_utils';
import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
export default class ProtectedBranchEdit {
......@@ -38,29 +38,25 @@ export default class ProtectedBranchEdit {
this.$allowedToMergeDropdown.disable();
this.$allowedToPushDropdown.disable();
$.ajax({
type: 'POST',
url: this.$wrap.data('url'),
dataType: 'json',
data: {
_method: 'PATCH',
protected_branch: {
merge_access_levels_attributes: [{
id: this.$allowedToMergeDropdown.data('access-level-id'),
access_level: $allowedToMergeInput.val(),
}],
push_access_levels_attributes: [{
id: this.$allowedToPushDropdown.data('access-level-id'),
access_level: $allowedToPushInput.val(),
}],
},
axios.patch(this.$wrap.data('url'), {
protected_branch: {
merge_access_levels_attributes: [{
id: this.$allowedToMergeDropdown.data('access-level-id'),
access_level: $allowedToMergeInput.val(),
}],
push_access_levels_attributes: [{
id: this.$allowedToPushDropdown.data('access-level-id'),
access_level: $allowedToPushInput.val(),
}],
},
error() {
new Flash('Failed to update branch!', 'alert', document.querySelector('.js-protected-branches-list'));
},
}).always(() => {
}).then(() => {
this.$allowedToMergeDropdown.enable();
this.$allowedToPushDropdown.enable();
}).catch(() => {
this.$allowedToMergeDropdown.enable();
this.$allowedToPushDropdown.enable();
flash('Failed to update branch!', 'alert', document.querySelector('.js-protected-branches-list'));
});
}
}
/* eslint-disable no-new */
import Flash from '../flash';
import flash from '../flash';
import axios from '../lib/utils/axios_utils';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
export default class ProtectedTagEdit {
......@@ -28,24 +28,19 @@ export default class ProtectedTagEdit {
this.$allowedToCreateDropdownButton.disable();
$.ajax({
type: 'POST',
url: this.$wrap.data('url'),
dataType: 'json',
data: {
_method: 'PATCH',
protected_tag: {
create_access_levels_attributes: [{
id: this.$allowedToCreateDropdownButton.data('access-level-id'),
access_level: $allowedToCreateInput.val(),
}],
},
axios.patch(this.$wrap.data('url'), {
protected_tag: {
create_access_levels_attributes: [{
id: this.$allowedToCreateDropdownButton.data('access-level-id'),
access_level: $allowedToCreateInput.val(),
}],
},
error() {
new Flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list'));
},
}).always(() => {
}).then(() => {
this.$allowedToCreateDropdownButton.enable();
}).catch(() => {
this.$allowedToCreateDropdownButton.enable();
flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list'));
});
}
}
......@@ -2,6 +2,8 @@
import _ from 'underscore';
import Cookies from 'js-cookie';
import flash from './flash';
import axios from './lib/utils/axios_utils';
function Sidebar(currentUser) {
this.toggleTodo = this.toggleTodo.bind(this);
......@@ -62,7 +64,7 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
Sidebar.prototype.toggleTodo = function(e) {
var $btnText, $this, $todoLoading, ajaxType, url;
$this = $(e.currentTarget);
ajaxType = $this.attr('data-delete-path') ? 'DELETE' : 'POST';
ajaxType = $this.attr('data-delete-path') ? 'delete' : 'post';
if ($this.attr('data-delete-path')) {
url = "" + ($this.attr('data-delete-path'));
} else {
......@@ -71,25 +73,14 @@ Sidebar.prototype.toggleTodo = function(e) {
$this.tooltip('hide');
return $.ajax({
url: url,
type: ajaxType,
dataType: 'json',
data: {
issuable_id: $this.data('issuable-id'),
issuable_type: $this.data('issuable-type')
},
beforeSend: (function(_this) {
return function() {
$('.js-issuable-todo').disable()
.addClass('is-loading');
};
})(this)
}).done((function(_this) {
return function(data) {
return _this.todoUpdateDone(data);
};
})(this));
$('.js-issuable-todo').disable().addClass('is-loading');
axios[ajaxType](url, {
issuable_id: $this.data('issuable-id'),
issuable_type: $this.data('issuable-type'),
}).then(({ data }) => {
this.todoUpdateDone(data);
}).catch(() => flash(`There was an error ${ajaxType === 'post' ? 'adding a' : 'deleting the'} todo.`));
};
Sidebar.prototype.todoUpdateDone = function(data) {
......
import Cookies from 'js-cookie';
import Mousetrap from 'mousetrap';
import axios from './lib/utils/axios_utils';
import { refreshCurrentPage, visitUrl } from './lib/utils/url_utility';
import findAndFollowLink from './shortcuts_dashboard_navigation';
......@@ -85,21 +86,21 @@ export default class Shortcuts {
$modal.modal('toggle');
}
$.ajax({
url: gon.shortcuts_path,
dataType: 'script',
success() {
if (location && location.length > 0) {
const results = [];
for (let i = 0, len = location.length; i < len; i += 1) {
results.push($(location[i]).show());
}
return results;
return axios.get(gon.shortcuts_path, {
responseType: 'text',
}).then(({ data }) => {
$.globalEval(data);
if (location && location.length > 0) {
const results = [];
for (let i = 0, len = location.length; i < len; i += 1) {
results.push($(location[i]).show());
}
return results;
}
$('.hidden-shortcut').show();
return $('.js-more-help-button').remove();
},
$('.hidden-shortcut').show();
return $('.js-more-help-button').remove();
});
}
......
import 'deckar01-task_list';
import axios from './lib/utils/axios_utils';
import Flash from './flash';
export default class TaskList {
......@@ -7,11 +8,11 @@ export default class TaskList {
this.dataType = options.dataType;
this.fieldName = options.fieldName;
this.onSuccess = options.onSuccess || (() => {});
this.onError = function showFlash(response) {
this.onError = function showFlash(e) {
let errorMessages = '';
if (response.responseJSON) {
errorMessages = response.responseJSON.errors.join(' ');
if (e.response.data && typeof e.response.data === 'object') {
errorMessages = e.response.data.errors.join(' ');
}
return new Flash(errorMessages || 'Update failed', 'alert');
......@@ -38,12 +39,9 @@ export default class TaskList {
patchData[this.dataType] = {
[this.fieldName]: $target.val(),
};
return $.ajax({
type: 'PATCH',
url: $target.data('update-url') || $('form.js-issuable-update').attr('action'),
data: patchData,
success: this.onSuccess,
error: this.onError,
});
return axios.patch($target.data('update-url') || $('form.js-issuable-update').attr('action'), patchData)
.then(({ data }) => this.onSuccess(data))
.catch(err => this.onError(err));
}
}
......@@ -8,7 +8,7 @@ import { convertPermissionToBoolean } from './lib/utils/common_utils';
```
%button.js-project-feature-toggle.project-feature-toggle{ type: "button",
class: "#{'is-checked' if enabled?}",
'aria-label': _('Toggle Cluster') }
'aria-label': _('Toggle Kubernetes Cluster') }
%input{ type: "hidden", class: 'js-project-feature-toggle-input', value: enabled? }
```
*/
......
import axios from '../lib/utils/axios_utils';
import Activities from '../activities';
import ActivityCalendar from './activity_calendar';
import { localTimeAgo } from '../lib/utils/datetime_utility';
import { __ } from '../locale';
import flash from '../flash';
/**
* UserTabs
......@@ -131,18 +134,20 @@ export default class UserTabs {
}
loadTab(action, endpoint) {
return $.ajax({
beforeSend: () => this.toggleLoading(true),
complete: () => this.toggleLoading(false),
dataType: 'json',
url: endpoint,
success: (data) => {
this.toggleLoading(true);
return axios.get(endpoint)
.then(({ data }) => {
const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html);
this.loaded[action] = true;
localTimeAgo($('.js-timeago', tabSelector));
},
});
this.toggleLoading(false);
})
.catch(() => {
this.toggleLoading(false);
});
}
loadActivities() {
......@@ -158,17 +163,15 @@ export default class UserTabs {
utcFormatted = `UTC${utcOffset > 0 ? '+' : ''}${(utcOffset / 3600)}`;
}
$.ajax({
dataType: 'json',
url: calendarPath,
success: (activityData) => {
axios.get(calendarPath)
.then(({ data }) => {
$calendarWrap.html(CALENDAR_TEMPLATE);
$calendarWrap.find('.calendar-hint').append(`(Timezone: ${utcFormatted})`);
// eslint-disable-next-line no-new
new ActivityCalendar('.js-contrib-calendar', activityData, calendarActivitiesPath, utcOffset);
},
});
new ActivityCalendar('.js-contrib-calendar', data, calendarActivitiesPath, utcOffset);
})
.catch(() => flash(__('There was an error loading users activity calendar.')));
// eslint-disable-next-line no-new
new Activities();
......
......@@ -2,6 +2,7 @@
/* global Issuable */
/* global emitSidebarEvent */
import _ from 'underscore';
import axios from './lib/utils/axios_utils';
// TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
......@@ -177,32 +178,28 @@ function UsersSelect(currentUser, els, options = {}) {
$loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown');
return $.ajax({
type: 'PUT',
dataType: 'json',
url: issueURL,
data: data
}).done(function(data) {
var user;
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
if (data.assignee) {
user = {
name: data.assignee.name,
username: data.assignee.username,
avatar: data.assignee.avatar_url
};
} else {
user = {
name: 'Unassigned',
username: '',
avatar: ''
};
}
$value.html(assigneeTemplate(user));
$collapsedSidebar.attr('title', _.escape(user.name)).tooltip('fixTitle');
return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
});
return axios.put(issueURL, data)
.then(({ data }) => {
var user;
$dropdown.trigger('loaded.gl.dropdown');
$loading.fadeOut();
if (data.assignee) {
user = {
name: data.assignee.name,
username: data.assignee.username,
avatar: data.assignee.avatar_url
};
} else {
user = {
name: 'Unassigned',
username: '',
avatar: ''
};
}
$value.html(assigneeTemplate(user));
$collapsedSidebar.attr('title', _.escape(user.name)).tooltip('fixTitle');
return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
});
};
collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>');
assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>');
......@@ -660,38 +657,33 @@ UsersSelect.prototype.user = function(user_id, callback) {
var url;
url = this.buildUrl(this.userPath);
url = url.replace(':id', user_id);
return $.ajax({
url: url,
dataType: "json"
}).done(function(user) {
return callback(user);
});
return axios.get(url)
.then(({ data }) => {
callback(data);
});
};
// Return users list. Filtered by query
// Only active users retrieved
UsersSelect.prototype.users = function(query, options, callback) {
var url;
url = this.buildUrl(this.usersPath);
return $.ajax({
url: url,
data: {
search: query,
per_page: options.perPage || 20,
active: true,
project_id: options.projectId || null,
group_id: options.groupId || null,
skip_ldap: options.skipLdap || null,
todo_filter: options.todoFilter || null,
todo_state_filter: options.todoStateFilter || null,
current_user: options.showCurrentUser || null,
author_id: options.authorId || null,
skip_users: options.skipUsers || null
},
dataType: "json"
}).done(function(users) {
return callback(users);
});
const url = this.buildUrl(this.usersPath);
const params = {
search: query,
per_page: options.perPage || 20,
active: true,
project_id: options.projectId || null,
group_id: options.groupId || null,
skip_ldap: options.skipLdap || null,
todo_filter: options.todoFilter || null,
todo_state_filter: options.todoStateFilter || null,
current_user: options.showCurrentUser || null,
author_id: options.authorId || null,
skip_users: options.skipUsers || null
};
return axios.get(url, { params })
.then(({ data }) => {
callback(data);
});
};
UsersSelect.prototype.buildUrl = function(url) {
......
......@@ -118,7 +118,7 @@
<template>
<div class="branch-commit">
<template v-if="hasCommitRef && showBranch">
<div class="icon-container hidden-xs">
<div class="icon-container">
<i
v-if="tag"
class="fa fa-tag"
......@@ -132,7 +132,7 @@
</div>
<a
class="ref-name hidden-xs"
class="ref-name"
:href="commitRef.ref_url"
v-tooltip
data-container="body"
......
......@@ -61,3 +61,4 @@
@import "framework/responsive_tables";
@import "framework/stacked-progress-bar";
@import "framework/ci_variable_list";
@import "framework/feature_highlight";
.feature-highlight {
position: relative;
margin-left: $gl-padding;
width: 20px;
height: 20px;
cursor: pointer;
&::before {
content: '';
display: block;
position: absolute;
top: 6px;
left: 6px;
width: 8px;
height: 8px;
background-color: $blue-500;
border-radius: 50%;
box-shadow: 0 0 0 rgba($blue-500, 0.4);
animation: pulse-highlight 2s infinite;
}
&:hover::before,
&.disable-animation::before {
animation: none;
}
&[disabled]::before {
display: none;
}
}
.is-showing-fly-out {
.feature-highlight {
display: none;
}
}
.feature-highlight-popover-content {
display: none;
hr {
margin: $gl-padding * 0.5 0;
}
.btn-link {
svg {
@include btn-svg;
path {
fill: currentColor;
}
}
}
.feature-highlight-illustration {
width: 100%;
height: 100px;
padding-top: 12px;
padding-bottom: 12px;
background-color: $indigo-50;
border-top-left-radius: 2px;
border-top-right-radius: 2px;
border-bottom: 1px solid darken($gray-normal, 8%);
}
}
.popover .feature-highlight-popover-content {
display: block;
}
.feature-highlight-popover {
width: 240px;
padding: 0;
border: 1px solid $dropdown-border-color;
box-shadow: 0 2px 4px $dropdown-shadow-color;
&.right > .arrow {
border-right-color: $dropdown-border-color;
}
.popover-content {
padding: 0;
}
}
.feature-highlight-popover-sub-content {
padding: 9px 14px;
}
@include keyframes(pulse-highlight) {
0% {
box-shadow: 0 0 0 0 rgba($blue-200, 0.4);
}
70% {
box-shadow: 0 0 0 10px transparent;
}
100% {
box-shadow: 0 0 0 0 transparent;
}
}
......@@ -4,6 +4,11 @@
.page-title {
margin-top: 0;
.color-label {
font-size: $gl-font-size;
padding: $gl-vert-padding $label-padding-modal;
}
}
}
......
......@@ -558,6 +558,7 @@ $jq-ui-default-color: #777;
* Label
*/
$label-padding: 7px;
$label-padding-modal: 10px;
$label-gray-bg: #f8fafc;
$label-inverse-bg: #333;
$label-remove-border: rgba(0, 0, 0, .1);
......
......@@ -224,3 +224,16 @@
border-radius: $label-border-radius;
font-weight: $gl-font-weight-normal;
}
.js-groups-dropdown {
width: 100%;
}
.dropdown-group-transfer {
bottom: 100%;
top: initial;
.dropdown-content {
overflow-y: unset;
}
}
......@@ -58,13 +58,13 @@
@media (min-width: $screen-sm-min) {
width: 200px;
margin-left: $gl-padding * 2;
margin-bottom: 0;
}
.label {
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
max-width: 100%;
}
}
......@@ -79,26 +79,33 @@
width: 100px;
margin-left: 10px;
margin-bottom: 0;
vertical-align: middle;
vertical-align: top;
}
}
.label-description {
display: block;
margin-bottom: 10px;
margin-left: 50px;
.description-text {
margin-bottom: $gl-padding;
}
a {
color: $blue-600;
}
@media (min-width: $screen-sm-min) {
display: inline-block;
width: 30%;
max-width: 50%;
margin-left: 10px;
margin-bottom: 0;
vertical-align: middle;
vertical-align: top;
}
}
.label {
padding: 8px 9px 9px;
padding: 8px 12px;
font-size: 14px;
}
}
......@@ -116,6 +123,12 @@
}
.manage-labels-list {
@media(min-width: $screen-md-min) {
&.content-list li {
padding: $gl-padding 0;
}
}
> li:not(.empty-message):not(.is-not-draggable) {
background-color: $white-light;
cursor: move;
......@@ -133,8 +146,6 @@
}
.btn-action {
color: $gl-text-color;
.fa {
font-size: 18px;
vertical-align: middle;
......@@ -155,10 +166,18 @@
float: right;
}
}
@media (max-width: $screen-xs-max) {
.dropdown-menu {
min-width: 100%;
}
}
}
.draggable-handler {
display: inline-block;
vertical-align: top;
margin: 5px 0;
opacity: 0;
transition: opacity .3s;
color: $gray-darkest;
......@@ -188,7 +207,7 @@
.toggle-priority {
display: inline-block;
vertical-align: middle;
vertical-align: top;
button {
border-color: transparent;
......@@ -255,6 +274,11 @@
}
.label-subscribe-button {
@media(min-width: $screen-md-min) {
min-width: 105px;
margin-left: $gl-padding;
}
.label-subscribe-button-icon {
&[disabled] {
opacity: 0.5;
......
......@@ -121,7 +121,7 @@
.ref-name {
font-weight: $gl-font-weight-bold;
max-width: 120px;
max-width: 100px;
overflow: hidden;
display: inline-block;
white-space: nowrap;
......
......@@ -6,6 +6,14 @@
}
}
.wiki-form {
.edit-wiki-page-slug-tip {
display: inline-block;
max-width: 100%;
margin-top: 5px;
}
}
.title .edit-wiki-header {
width: 780px;
margin-left: auto;
......
class Admin::BroadcastMessagesController < Admin::ApplicationController
include BroadcastMessagesHelper
before_action :finder, only: [:edit, :update, :destroy]
def index
......@@ -37,7 +39,8 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
end
def preview
@broadcast_message = BroadcastMessage.new(broadcast_message_params)
broadcast_message = BroadcastMessage.new(broadcast_message_params)
render json: { message: render_broadcast_message(broadcast_message) }
end
protected
......
......@@ -70,7 +70,7 @@ module UploadsActions
end
def build_uploader_from_params
uploader = uploader_class.new(model, params[:secret])
uploader = uploader_class.new(model, secret: params[:secret])
uploader.retrieve_from_store!(params[:filename])
uploader
end
......
......@@ -54,7 +54,7 @@ class Groups::LabelsController < Groups::ApplicationController
respond_to do |format|
format.html do
redirect_to group_labels_path(@group), status: 302, notice: 'Label was removed'
redirect_to group_labels_path(@group), status: 302, notice: "#{@label.name} deleted permanently"
end
format.js
end
......
......@@ -10,7 +10,7 @@ class GroupsController < Groups::ApplicationController
before_action :group, except: [:index, :new, :create]
# Authorize
before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects]
before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects, :transfer]
before_action :authorize_create_group!, only: [:new]
before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests]
......@@ -94,6 +94,19 @@ class GroupsController < Groups::ApplicationController
redirect_to root_path, status: 302, alert: "Group '#{@group.name}' was scheduled for deletion."
end
def transfer
parent_group = Group.find_by(id: params[:new_parent_group_id])
service = ::Groups::TransferService.new(@group, current_user)
if service.execute(parent_group)
flash[:notice] = "Group '#{@group.name}' was successfully transferred."
redirect_to group_path(@group)
else
flash.now[:alert] = service.error
render :edit
end
end
protected
def authorize_create_group!
......
......@@ -42,7 +42,7 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
when 'true'
return
when 'false'
flash[:alert] = _('Please <a href=%{link_to_billing} target="_blank" rel="noopener noreferrer">enable billing for one of your projects to be able to create a cluster</a>, then try again.').html_safe % { link_to_billing: "https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral" }
flash[:alert] = _('Please <a href=%{link_to_billing} target="_blank" rel="noopener noreferrer">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again.').html_safe % { link_to_billing: "https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral" }
else
flash[:alert] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.')
end
......
......@@ -41,7 +41,7 @@ class Projects::ClustersController < Projects::ApplicationController
head :no_content
end
format.html do
flash[:notice] = "Cluster was successfully updated."
flash[:notice] = _('Kubernetes cluster was successfully updated.')
redirect_to project_cluster_path(project, cluster)
end
end
......@@ -55,10 +55,10 @@ class Projects::ClustersController < Projects::ApplicationController
def destroy
if cluster.destroy
flash[:notice] = "Cluster integration was successfully removed."
flash[:notice] = _('Kubernetes cluster integration was successfully removed.')
redirect_to project_clusters_path(project), status: 302
else
flash[:notice] = "Cluster integration was not removed."
flash[:notice] = _('Kubernetes cluster integration was not removed.')
render :show
end
end
......
......@@ -5,6 +5,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
rescue_from Gitlab::GitAccess::UnauthorizedError, with: :render_403
rescue_from Gitlab::GitAccess::NotFoundError, with: :render_404
rescue_from Gitlab::GitAccess::ProjectCreationError, with: :render_422
# GET /foo/bar.git/info/refs?service=git-upload-pack (git pull)
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
......@@ -55,8 +56,15 @@ class Projects::GitHttpController < Projects::GitHttpClientController
render plain: exception.message, status: :not_found
end
def render_422(exception)
render plain: exception.message, status: :unprocessable_entity
end
def access
@access ||= access_klass.new(access_actor, project, 'http', authentication_abilities: authentication_abilities, redirected_path: redirected_path)
@access ||= access_klass.new(access_actor, project,
'http', authentication_abilities: authentication_abilities,
namespace_path: params[:namespace_id], project_path: project_path,
redirected_path: redirected_path)
end
def access_actor
......@@ -68,12 +76,17 @@ class Projects::GitHttpController < Projects::GitHttpClientController
# Use the magic string '_any' to indicate we do not know what the
# changes are. This is also what gitlab-shell does.
access.check(git_command, '_any')
@project ||= access.project
end
def access_klass
@access_klass ||= wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess
end
def project_path
@project_path ||= params[:project_id].sub(/\.git$/, '')
end
def log_user_activity
Users::ActivityService.new(user, 'pull').execute
end
......
......@@ -54,8 +54,8 @@ class Projects::WikisController < Projects::ApplicationController
else
render 'edit'
end
rescue WikiPage::PageChangedError
@conflict = true
rescue WikiPage::PageChangedError, WikiPage::PageRenameError => e
@error = e
render 'edit'
end
......@@ -76,9 +76,9 @@ class Projects::WikisController < Projects::ApplicationController
@page = @project_wiki.find_page(params[:id])
if @page
@page_versions = Kaminari.paginate_array(@page.versions(page: params[:page]),
@page_versions = Kaminari.paginate_array(@page.versions(page: params[:page].to_i),
total_count: @page.count_versions)
.page(params[:page])
.page(params[:page])
else
redirect_to(
project_wiki_path(@project, :home),
......
class UserCalloutsController < ApplicationController
def create
if ensure_callout.persisted?
respond_to do |format|
format.json { head :ok }
end
else
respond_to do |format|
format.json { head :bad_request }
end
end
end
private
def ensure_callout
current_user.callouts.find_or_create_by(feature_name: UserCallout.feature_names[feature_name])
end
def feature_name
params.require(:feature_name)
end
end
module GroupsHelper
def group_nav_link_paths
%w[groups#projects groups#edit ci_cd#show ldap_group_links#index hooks#index audit_events#index pipeline_quota#index]
end
def can_change_group_visibility_level?(group)
can?(current_user, :change_visibility_level, group)
end
......@@ -88,6 +92,19 @@ module GroupsHelper
end
end
def parent_group_options(current_group)
groups = current_user.owned_groups.sort_by(&:human_name).map do |group|
{ id: group.id, text: group.human_name }
end
groups.delete_if { |group| group[:id] == current_group.id }
groups.to_json
end
def supports_nested_groups?
Group.supports_nested_groups?
end
private
def group_title_link(group, hidable: false, show_avatar: false, for_dropdown: false)
......
module UserCalloutsHelper
GKE_CLUSTER_INTEGRATION = 'gke_cluster_integration'.freeze
def show_gke_cluster_integration_callout?(project)
can?(current_user, :create_cluster, project) &&
!user_dismissed?(GKE_CLUSTER_INTEGRATION)
end
private
def user_dismissed?(feature_name)
current_user&.callouts&.find_by(feature_name: UserCallout.feature_names[feature_name])
end
end
......@@ -21,4 +21,22 @@ module WikiHelper
add_to_breadcrumb_dropdown link_to(WikiPage.unhyphenize(dir_or_page).capitalize, project_wiki_path(@project, current_slug)), location: :after
end
end
def wiki_page_errors(error)
return unless error
content_tag(:div, class: 'alert alert-danger') do
case error
when WikiPage::PageChangedError
page_link = link_to s_("WikiPageConflictMessage|the page"), project_wiki_path(@project, @page), target: "_blank"
concat(
(s_("WikiPageConflictMessage|Someone edited the page the same time you did. Please check out %{page_link} and make sure your changes will not unintentionally remove theirs.") % { page_link: page_link }).html_safe
)
when WikiPage::PageRenameError
s_("WikiEdit|There is already a page with the same title in that path.")
else
error.message
end
end
end
end
......@@ -21,6 +21,7 @@ module Ci
has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :job_artifacts_archive, -> { where(file_type: Ci::JobArtifact.file_types[:archive]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :job_artifacts_metadata, -> { where(file_type: Ci::JobArtifact.file_types[:metadata]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
has_one :job_artifacts_trace, -> { where(file_type: Ci::JobArtifact.file_types[:trace]) }, class_name: 'Ci::JobArtifact', inverse_of: :job, foreign_key: :job_id
# The "environment" field for builds is a String, and is the unexpanded name
def persisted_environment
......
......@@ -9,9 +9,12 @@ module Ci
mount_uploader :file, JobArtifactUploader
delegate :open, :exists?, to: :file
enum file_type: {
archive: 1,
metadata: 2
metadata: 2,
trace: 3
}
def self.artifacts_size_for(project)
......
......@@ -180,7 +180,7 @@ module Clusters
return unless managed?
if api_url_changed? || token_changed? || ca_pem_changed?
errors.add(:base, "cannot modify managed cluster")
errors.add(:base, _('Cannot modify managed Kubernetes cluster'))
return false
end
......
......@@ -39,7 +39,6 @@ module ArtifactMigratable
end
def artifacts_size
read_attribute(:artifacts_size).to_i +
job_artifacts_archive&.size.to_i + job_artifacts_metadata&.size.to_i
read_attribute(:artifacts_size).to_i + job_artifacts.sum(:size).to_i
end
end
......@@ -7,11 +7,12 @@ module Routable
has_one :route, as: :source, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :redirect_routes, as: :source, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
validates_associated :route
validates :route, presence: true
scope :with_route, -> { includes(:route) }
after_validation :set_path_errors
before_validation do
if full_path_changed? || full_name_changed?
prepare_route
......@@ -125,6 +126,11 @@ module Routable
private
def set_path_errors
route_path_errors = self.errors.delete(:"route.path")
self.errors[:path].concat(route_path_errors) if route_path_errors
end
def uncached_full_path
if route && route.path.present?
@full_path ||= route.path # rubocop:disable Gitlab/ModuleWithInstanceVariables
......
......@@ -14,7 +14,11 @@ module Storage
# Ensure old directory exists before moving it
gitlab_shell.add_namespace(repository_storage_path, full_path_was)
# Ensure new directory exists before moving it (if there's a parent)
gitlab_shell.add_namespace(repository_storage_path, parent.full_path) if parent
unless gitlab_shell.mv_namespace(repository_storage_path, full_path_was, full_path)
Rails.logger.error "Exception moving path #{repository_storage_path} from #{full_path_was} to #{full_path}"
# if we cannot move namespace directory we should rollback
......
......@@ -20,6 +20,9 @@ class Namespace < ActiveRecord::Base
has_many :projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :project_statistics
# This should _not_ be `inverse_of: :namespace`, because that would also set
# `user.namespace` when this user creates a group with themselves as `owner`.
belongs_to :owner, class_name: "User"
belongs_to :parent, class_name: "Namespace"
......@@ -29,7 +32,6 @@ class Namespace < ActiveRecord::Base
validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
validates :name,
presence: true,
uniqueness: { scope: :parent_id },
length: { maximum: 255 },
namespace_name: true
......@@ -40,7 +42,6 @@ class Namespace < ActiveRecord::Base
namespace_path: true
validate :nesting_level_allowed
validate :allowed_path_by_redirects
delegate :name, to: :owner, allow_nil: true, prefix: true
......@@ -52,7 +53,7 @@ class Namespace < ActiveRecord::Base
# Legacy Storage specific hooks
after_update :move_dir, if: :path_changed?
after_update :move_dir, if: :path_or_parent_changed?
before_destroy(prepend: true) { prepare_for_destroy }
after_destroy :rm_dir
......@@ -222,9 +223,12 @@ class Namespace < ActiveRecord::Base
end
def full_path_was
return path_was unless has_parent?
"#{parent.full_path}/#{path_was}"
if parent_id_was.nil?
path_was
else
previous_parent = Group.find_by(id: parent_id_was)
previous_parent.full_path + '/' + path_was
end
end
# Exports belonging to projects with legacy storage are placed in a common
......@@ -241,6 +245,10 @@ class Namespace < ActiveRecord::Base
private
def path_or_parent_changed?
path_changed? || parent_changed?
end
def refresh_access_of_projects_invited_groups
Group
.joins(project_group_links: :project)
......@@ -271,16 +279,6 @@ class Namespace < ActiveRecord::Base
.update_all(share_with_group_lock: true)
end
def allowed_path_by_redirects
return if path.nil?
errors.add(:path, "#{path} has been taken before. Please use another one") if namespace_previously_created_with_same_path?
end
def namespace_previously_created_with_same_path?
RedirectRoute.permanent.exists?(path: path)
end
def write_projects_repository_config
all_projects.find_each do |project|
project.expires_full_path_cache # we need to clear cache to validate renames correctly
......
......@@ -60,7 +60,7 @@ class Note < ActiveRecord::Base
belongs_to :updated_by, class_name: "User"
belongs_to :last_edited_by, class_name: 'User'
has_many :todos, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :todos
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :system_note_metadata
......
......@@ -245,8 +245,7 @@ class Project < ActiveRecord::Base
validates :path,
presence: true,
project_path: true,
length: { maximum: 255 },
uniqueness: { scope: :namespace_id }
length: { maximum: 255 }
validates :namespace, presence: true
validates :name, uniqueness: { scope: :namespace_id }
......@@ -511,10 +510,13 @@ class Project < ActiveRecord::Base
@repository ||= Repository.new(full_path, self, disk_path: disk_path)
end
def reload_repository!
def cleanup
@repository&.cleanup
@repository = nil
end
alias_method :reload_repository!, :cleanup
def container_registry_url
if Gitlab.config.registry.enabled
"#{Gitlab.config.registry.host_port}/#{full_path.downcase}"
......
......@@ -150,9 +150,10 @@ class KubernetesService < DeploymentService
end
def deprecation_message
content = <<-MESSAGE.strip_heredoc
Kubernetes service integration has been deprecated. #{deprecated_message_content} your clusters using the new <a href=\'#{Gitlab::Routing.url_helpers.project_clusters_path(project)}'/>Clusters</a> page
MESSAGE
content = _("Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page") % {
deprecated_message_content: deprecated_message_content,
url: Gitlab::Routing.url_helpers.project_clusters_path(project)
}
content.html_safe
end
......@@ -248,9 +249,9 @@ class KubernetesService < DeploymentService
def deprecated_message_content
if active?
"Your cluster information on this page is still editable, but you are advised to disable and reconfigure"
_("Your Kubernetes cluster information on this page is still editable, but you are advised to disable and reconfigure")
else
"Fields on this page are now uneditable, you can configure"
_("Fields on this page are now uneditable, you can configure")
end
end
end
......@@ -119,6 +119,8 @@ class ProjectWiki
end
def delete_page(page, message = nil)
return unless page
wiki.delete_page(page.path, commit_details(:deleted, message, page.title))
update_project_activity
......@@ -131,6 +133,8 @@ class ProjectWiki
end
def page_title_and_dir(title)
return unless title
title_array = title.split("/")
title = title_array.pop
[title, title_array.join("/")]
......
......@@ -93,6 +93,10 @@ class Repository
alias_method :raw, :raw_repository
def cleanup
@raw_repository&.cleanup
end
# Return absolute path to repository
def path_to_repo
@path_to_repo ||= File.expand_path(
......
......@@ -75,7 +75,7 @@ class Route < ActiveRecord::Base
def ensure_permanent_paths
return if path.nil?
errors.add(:path, "#{path} has been taken before. Please use another one") if conflicting_redirect_exists?
errors.add(:path, "has been taken before") if conflicting_redirect_exists?
end
def conflicting_redirect_exists?
......
......@@ -28,6 +28,7 @@ class Todo < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true, allow_nil: true
validates :action, :project, :target_type, :user, presence: true
validates :author, presence: true
validates :target_id, presence: true, unless: :for_commit?
validates :commit_id, presence: true, if: :for_commit?
......
......@@ -12,6 +12,10 @@ class Upload < ActiveRecord::Base
before_save :calculate_checksum!, if: :foreground_checksummable?
after_commit :schedule_checksum, if: :checksummable?
# as the FileUploader is not mounted, the default CarrierWave ActiveRecord
# hooks are not executed and the file will not be deleted
after_destroy :delete_file!, if: -> { uploader_class <= FileUploader }
def self.hexdigest(path)
Digest::SHA256.file(path).hexdigest
end
......@@ -30,7 +34,7 @@ class Upload < ActiveRecord::Base
end
def build_uploader
uploader_class.new(model).tap do |uploader|
uploader_class.new(model, mount_point, **uploader_context).tap do |uploader|
uploader.upload = self
uploader.retrieve_from_store!(identifier)
end
......@@ -40,8 +44,19 @@ class Upload < ActiveRecord::Base
File.exist?(absolute_path)
end
def uploader_context
{
identifier: identifier,
secret: secret
}.compact
end
private
def delete_file!
build_uploader.remove!
end
def checksummable?
checksum.nil? && local? && exist?
end
......@@ -62,11 +77,15 @@ class Upload < ActiveRecord::Base
!path.start_with?('/')
end
def uploader_class
Object.const_get(uploader)
end
def identifier
File.basename(path)
end
def uploader_class
Object.const_get(uploader)
def mount_point
super&.to_sym
end
end
......@@ -77,7 +77,7 @@ class User < ActiveRecord::Base
#
# Namespace for personal projects
has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, autosave: true # rubocop:disable Cop/ActiveRecordDependent
has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, inverse_of: :owner, autosave: true # rubocop:disable Cop/ActiveRecordDependent
# Profile
has_many :keys, -> do
......@@ -125,7 +125,7 @@ class User < ActiveRecord::Base
has_many :spam_logs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :builds, dependent: :nullify, class_name: 'Ci::Build' # rubocop:disable Cop/ActiveRecordDependent
has_many :pipelines, dependent: :nullify, class_name: 'Ci::Pipeline' # rubocop:disable Cop/ActiveRecordDependent
has_many :todos, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :todos
has_many :notification_settings, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :award_emoji, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :owner_id # rubocop:disable Cop/ActiveRecordDependent
......@@ -135,6 +135,7 @@ class User < ActiveRecord::Base
has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest" # rubocop:disable Cop/ActiveRecordDependent
has_many :custom_attributes, class_name: 'UserCustomAttribute'
has_many :callouts, class_name: 'UserCallout'
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
#
......@@ -150,12 +151,9 @@ class User < ActiveRecord::Base
validates :projects_limit,
presence: true,
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
validates :username,
user_path: true,
presence: true,
uniqueness: { case_sensitive: false }
validates :username, presence: true
validate :namespace_uniq, if: :username_changed?
validates :namespace, presence: true
validate :namespace_move_dir_allowed, if: :username_changed?
validate :unique_email, if: :email_changed?
......@@ -170,7 +168,8 @@ class User < ActiveRecord::Base
before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? }
before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
after_save :ensure_namespace_correct
before_validation :ensure_namespace_correct
after_validation :set_username_errors
after_update :username_changed_hook, if: :username_changed?
after_destroy :post_destroy_hook
after_destroy :remove_key_cache
......@@ -229,8 +228,8 @@ class User < ActiveRecord::Base
scope :active, -> { with_state(:active).non_internal }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'ASC')) }
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
def self.with_two_factor
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id")
......@@ -504,17 +503,6 @@ class User < ActiveRecord::Base
end
end
def namespace_uniq
# Return early if username already failed the first uniqueness validation
return if errors.key?(:username) &&
errors[:username].include?('has already been taken')
existing_namespace = Namespace.by_path(username)
if existing_namespace && existing_namespace != namespace
errors.add(:username, 'has already been taken')
end
end
def namespace_move_dir_allowed
if namespace&.any_project_has_container_registry_tags?
errors.add(:username, 'cannot be changed if a personal project has container registry tags.')
......@@ -883,19 +871,18 @@ class User < ActiveRecord::Base
end
def ensure_namespace_correct
# Ensure user has namespace
create_namespace!(path: username, name: username) unless namespace
if username_changed?
unless namespace.update_attributes(path: username, name: username)
namespace.errors.each do |attribute, message|
self.errors.add(:"namespace_#{attribute}", message)
end
raise ActiveRecord::RecordInvalid.new(namespace)
end
if namespace
namespace.path = namespace.name = username if username_changed?
else
build_namespace(path: username, name: username)
end
end
def set_username_errors
namespace_path_errors = self.errors.delete(:"namespace.path")
self.errors[:username].concat(namespace_path_errors) if namespace_path_errors
end
def username_changed_hook
system_hook_service.execute_hooks_for(self, :rename)
end
......
class UserCallout < ActiveRecord::Base
belongs_to :user
enum feature_name: {
gke_cluster_integration: 1
}
validates :user, presence: true
validates :feature_name,
presence: true,
uniqueness: { scope: :user_id },
inclusion: { in: UserCallout.feature_names.keys }
end
class WikiPage
PageChangedError = Class.new(StandardError)
PageRenameError = Class.new(StandardError)
include ActiveModel::Validations
include ActiveModel::Conversion
......@@ -102,7 +103,7 @@ class WikiPage
# The hierarchy of the directory this page is contained in.
def directory
wiki.page_title_and_dir(slug).last
wiki.page_title_and_dir(slug)&.last.to_s
end
# The processed/formatted content of this page.
......@@ -177,7 +178,7 @@ class WikiPage
# Creates a new Wiki Page.
#
# attr - Hash of attributes to set on the new page.
# :title - The title for the new page.
# :title - The title (optionally including dir) for the new page.
# :content - The raw markup content.
# :format - Optional symbol representing the
# content format. Can be any type
......@@ -189,7 +190,7 @@ class WikiPage
# Returns the String SHA1 of the newly created page
# or False if the save was unsuccessful.
def create(attrs = {})
@attributes.merge!(attrs)
update_attributes(attrs)
save(page_details: title) do
wiki.create_page(title, content, format, message)
......@@ -204,24 +205,29 @@ class WikiPage
# See ProjectWiki::MARKUPS Hash for available formats.
# :message - Optional commit message to set on the new version.
# :last_commit_sha - Optional last commit sha to validate the page unchanged.
# :title - The Title to replace existing title
# :title - The Title (optionally including dir) to replace existing title
#
# Returns the String SHA1 of the newly created page
# or False if the save was unsuccessful.
def update(attrs = {})
last_commit_sha = attrs.delete(:last_commit_sha)
if last_commit_sha && last_commit_sha != self.last_commit_sha
raise PageChangedError.new("You are attempting to update a page that has changed since you started editing it.")
raise PageChangedError
end
attrs.slice!(:content, :format, :message, :title)
@attributes.merge!(attrs)
page_details =
if title.present? && @page.title != title
title
else
@page.url_path
update_attributes(attrs)
if title_changed?
page_details = title
if wiki.find_page(page_details).present?
@attributes[:title] = @page.url_path
raise PageRenameError
end
else
page_details = @page.url_path
end
save(page_details: page_details) do
wiki.update_page(
......@@ -255,8 +261,44 @@ class WikiPage
page.version.to_s
end
def title_changed?
title.present? && self.class.unhyphenize(@page.url_path) != title
end
private
# Process and format the title based on the user input.
def process_title(title)
return if title.blank?
title = deep_title_squish(title)
current_dirname = File.dirname(title)
if @page.present?
return title[1..-1] if current_dirname == '/'
return File.join([directory.presence, title].compact) if current_dirname == '.'
end
title
end
# This method squishes all the filename
# i.e: ' foo / bar / page_name' => 'foo/bar/page_name'
def deep_title_squish(title)
components = title.split(File::SEPARATOR).map(&:squish)
File.join(components)
end
# Updates the current @attributes hash by merging a hash of params
def update_attributes(attrs)
attrs[:title] = process_title(attrs[:title]) if attrs[:title].present?
attrs.slice!(:content, :format, :message, :title)
@attributes.merge!(attrs)
end
def set_attributes
attributes[:slug] = @page.url_path
attributes[:title] = @page.title
......
module Ci
class CreateTraceArtifactService < BaseService
def execute(job)
return if job.job_artifacts_trace
job.trace.read do |stream|
if stream.file?
job.create_job_artifacts_trace!(
project: job.project,
file_type: :trace,
file: stream)
end
end
end
end
end
......@@ -5,7 +5,7 @@ module Clusters
def execute(access_token = nil)
@access_token = access_token
raise ArgumentError.new('Instance does not support multiple clusters') unless can_create_cluster?
raise ArgumentError.new(_('Instance does not support multiple Kubernetes clusters')) unless can_create_cluster?
create_cluster.tap do |cluster|
ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted?
......
......@@ -28,7 +28,7 @@ module Clusters
if elapsed_time_from_creation(operation) < TIMEOUT
WaitForClusterCreationWorker.perform_in(EAGER_INTERVAL, provider.cluster_id)
else
provider.make_errored!("Cluster creation time exceeds timeout; #{TIMEOUT}")
provider.make_errored!(_('Kubernetes cluster creation time exceeds timeout; %{timeout}') % { timeout: TIMEOUT })
end
end
......
module Files
class CreateService < Files::BaseService
def create_commit!
handler = Lfs::FileModificationHandler.new(project, @branch_name)
handler.new_file(@file_path, @file_content) do |content_or_lfs_pointer|
create_transformed_commit(content_or_lfs_pointer)
end
end
private
def create_transformed_commit(content_or_lfs_pointer)
repository.create_file(
current_user,
@file_path,
@file_content,
content_or_lfs_pointer,
message: @commit_message,
branch_name: @branch_name,
author_email: @author_email,
......
module Groups
class TransferService < Groups::BaseService
ERROR_MESSAGES = {
database_not_supported: 'Database is not supported.',
namespace_with_same_path: 'The parent group already has a subgroup with the same path.',
group_is_already_root: 'Group is already a root group.',
same_parent_as_current: 'Group is already associated to the parent group.',
invalid_policies: "You don't have enough permissions."
}.freeze
TransferError = Class.new(StandardError)
attr_reader :error
def initialize(group, user, params = {})
super
@error = nil
end
def execute(new_parent_group)
@new_parent_group = new_parent_group
ensure_allowed_transfer
proceed_to_transfer
rescue TransferError, ActiveRecord::RecordInvalid, Gitlab::UpdatePathError => e
@group.errors.clear
@error = "Transfer failed: " + e.message
false
end
private
def proceed_to_transfer
Group.transaction do
update_group_attributes
end
end
def ensure_allowed_transfer
raise_transfer_error(:group_is_already_root) if group_is_already_root?
raise_transfer_error(:database_not_supported) unless Group.supports_nested_groups?
raise_transfer_error(:same_parent_as_current) if same_parent?
raise_transfer_error(:invalid_policies) unless valid_policies?
raise_transfer_error(:namespace_with_same_path) if namespace_with_same_path?
end
def group_is_already_root?
!@new_parent_group && !@group.has_parent?
end
def same_parent?
@new_parent_group && @new_parent_group.id == @group.parent_id
end
def valid_policies?
return false unless can?(current_user, :admin_group, @group)
if @new_parent_group
can?(current_user, :create_subgroup, @new_parent_group)
else
can?(current_user, :create_group)
end
end
def namespace_with_same_path?
Namespace.exists?(path: @group.path, parent: @new_parent_group)
end
def update_group_attributes
if @new_parent_group && @new_parent_group.visibility_level < @group.visibility_level
update_children_and_projects_visibility
@group.visibility_level = @new_parent_group.visibility_level
end
@group.parent = @new_parent_group
@group.save!
end
def update_children_and_projects_visibility
descendants = @group.descendants.where("visibility_level > ?", @new_parent_group.visibility_level)
Group
.where(id: descendants.select(:id))
.update_all(visibility_level: @new_parent_group.visibility_level)
@group
.all_projects
.where("visibility_level > ?", @new_parent_group.visibility_level)
.update_all(visibility_level: @new_parent_group.visibility_level)
end
def raise_transfer_error(message)
raise TransferError, ERROR_MESSAGES[message]
end
end
end
module Lfs
class FileModificationHandler
attr_reader :project, :branch_name
delegate :repository, to: :project
def initialize(project, branch_name)
@project = project
@branch_name = branch_name
end
def new_file(file_path, file_content)
if project.lfs_enabled? && lfs_file?(file_path)
lfs_pointer_file = Gitlab::Git::LfsPointerFile.new(file_content)
lfs_object = create_lfs_object!(lfs_pointer_file, file_content)
content = lfs_pointer_file.pointer
success = yield(content)
link_lfs_object!(lfs_object) if success
else
yield(file_content)
end
end
private
def lfs_file?(file_path)
repository.attributes_at(branch_name, file_path)['filter'] == 'lfs'
end
def create_lfs_object!(lfs_pointer_file, file_content)
LfsObject.find_or_create_by(oid: lfs_pointer_file.sha256, size: lfs_pointer_file.size) do |lfs_object|
lfs_object.file = CarrierWaveStringFile.new(file_content)
end
end
def link_lfs_object!(lfs_object)
project.lfs_objects << lfs_object
end
end
end
......@@ -49,11 +49,11 @@ class FileMover
end
def uploader
@uploader ||= PersonalFileUploader.new(model, secret)
@uploader ||= PersonalFileUploader.new(model, secret: secret)
end
def temp_file_uploader
@temp_file_uploader ||= PersonalFileUploader.new(nil, secret)
@temp_file_uploader ||= PersonalFileUploader.new(nil, secret: secret)
end
def revert
......
......@@ -15,6 +15,8 @@ class FileUploader < GitlabUploader
storage :file
after :remove, :prune_store_dir
def self.root
File.join(options.storage_path, 'uploads')
end
......@@ -62,9 +64,11 @@ class FileUploader < GitlabUploader
attr_accessor :model
def initialize(model, secret = nil)
def initialize(model, mounted_as = nil, **uploader_context)
super(model, nil, **uploader_context)
@model = model
@secret = secret
apply_context!(uploader_context)
end
def base_dir
......@@ -107,15 +111,17 @@ class FileUploader < GitlabUploader
self.file.filename
end
# the upload does not hold the secret, but holds the path
# which contains the secret: extract it
def upload=(value)
super
return unless value
return if apply_context!(value.uploader_context)
# fallback to the regex based extraction
if matches = DYNAMIC_PATH_PATTERN.match(value.path)
@secret = matches[:secret]
@identifier = matches[:identifier]
end
super
end
def secret
......@@ -124,6 +130,22 @@ class FileUploader < GitlabUploader
private
def apply_context!(uploader_context)
@secret, @identifier = uploader_context.values_at(:secret, :identifier)
!!(@secret && @identifier)
end
def build_upload
super.tap do |upload|
upload.secret = secret
end
end
def prune_store_dir
storage.delete_dir!(store_dir) # only remove when empty
end
def markdown_name
(image_or_video? ? File.basename(filename, File.extname(filename)) : filename).gsub("]", "\\]")
end
......
......@@ -29,6 +29,10 @@ class GitlabUploader < CarrierWave::Uploader::Base
delegate :base_dir, :file_storage?, to: :class
def initialize(model, mounted_as = nil, **uploader_context)
super(model, mounted_as)
end
def file_cache_storage?
cache_storage.is_a?(CarrierWave::Storage::File)
end
......
......@@ -13,6 +13,12 @@ class JobArtifactUploader < GitlabUploader
dynamic_segment
end
def open
raise 'Only File System is supported' unless file_storage?
File.open(path, "rb") if path
end
private
def dynamic_segment
......
......@@ -24,7 +24,7 @@ module RecordsUploads
uploads.where(path: upload_path).delete_all
upload.destroy! if upload
self.upload = build_upload_from_uploader(self)
self.upload = build_upload
upload.save!
end
end
......@@ -39,12 +39,13 @@ module RecordsUploads
Upload.order(id: :desc).where(uploader: self.class.to_s)
end
def build_upload_from_uploader(uploader)
def build_upload
Upload.new(
size: uploader.file.size,
path: uploader.upload_path,
model: uploader.model,
uploader: uploader.class.to_s
uploader: self.class.to_s,
size: file.size,
path: upload_path,
model: model,
mount_point: mounted_as
)
end
......
......@@ -13,10 +13,6 @@ class AbstractPathValidator < ActiveModel::EachValidator
raise NotImplementedError
end
def self.full_path(record, value)
value
end
def self.valid_path?(path)
encode!(path)
"#{path}/" =~ path_regex
......@@ -28,7 +24,7 @@ class AbstractPathValidator < ActiveModel::EachValidator
return
end
full_path = self.class.full_path(record, value)
full_path = record.build_full_path
return unless full_path
unless self.class.valid_path?(full_path)
......
......@@ -12,8 +12,4 @@ class NamespacePathValidator < AbstractPathValidator
def self.format_error_message
Gitlab::PathRegex.namespace_format_message
end
def self.full_path(record, value)
record.build_full_path
end
end
......@@ -12,8 +12,4 @@ class ProjectPathValidator < AbstractPathValidator
def self.format_error_message
Gitlab::PathRegex.project_path_format_message
end
def self.full_path(record, value)
record.build_full_path
end
end
class UserPathValidator < AbstractPathValidator
extend Gitlab::EncodingHelper
def self.path_regex
Gitlab::PathRegex.root_namespace_path_regex
end
def self.format_regex
Gitlab::PathRegex.namespace_format_regex
end
def self.format_error_message
Gitlab::PathRegex.namespace_format_message
end
end
$('.js-broadcast-message-preview').html("#{j(render_broadcast_message(@broadcast_message))}");
......@@ -57,4 +57,20 @@
.form-actions
= button_to 'Remove group', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_group_message(@group) }
- if supports_nested_groups?
.panel.panel-warning
.panel-heading Transfer group
.panel-body
= form_for @group, url: transfer_group_path(@group), method: :put do |f|
.form-group
= dropdown_tag('Select parent group', options: { toggle_class: 'js-groups-dropdown', title: 'Parent Group', filter: true, dropdown_class: 'dropdown-open-top dropdown-group-transfer', placeholder: "Search groups", data: { data: parent_group_options(@group) } })
= hidden_field_tag 'new_parent_group_id'
%ul
%li Be careful. Changing a group's parent can have unintended #{link_to 'side effects', 'https://docs.gitlab.com/ce/user/project/index.html#redirects-when-changing-repository-paths', target: 'blank'}.
%li You can only transfer the group to a group you manage.
%li You will need to update your local repositories to point to the new location.
%li If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility.
= f.submit 'Transfer group', class: "btn btn-warning"
= render 'shared/confirm_modal', phrase: @group.path
......@@ -88,7 +88,7 @@
%strong.fly-out-top-item-name
#{ _('Members') }
- if current_user && can?(current_user, :admin_group, @group)
= nav_link(path: %w[groups#projects groups#edit ci_cd#show]) do
= nav_link(path: group_nav_link_paths) do
= link_to edit_group_path(@group) do
.nav-icon-container
= sprite_icon('settings')
......
......@@ -184,10 +184,33 @@
Environments
- if project_nav_tab? :clusters
- show_cluster_hint = show_gke_cluster_integration_callout?(@project)
= nav_link(controller: [:clusters, :user, :gcp]) do
= link_to project_clusters_path(@project), title: 'Cluster', class: 'shortcuts-cluster' do
= link_to project_clusters_path(@project), title: _('Kubernetes'), class: 'shortcuts-cluster' do
%span
Clusters
= _('Kubernetes')
- if show_cluster_hint
.feature-highlight.js-feature-highlight{ disabled: true,
data: { trigger: 'manual',
container: 'body',
toggle: 'popover',
placement: 'right',
highlight: UserCalloutsHelper::GKE_CLUSTER_INTEGRATION,
highlight_priority: UserCallout.feature_names[:GKE_CLUSTER_INTEGRATION],
dismiss_endpoint: user_callouts_path } }
- if show_cluster_hint
.feature-highlight-popover-content
= image_tag 'illustrations/cluster_popover.svg', class: 'feature-highlight-illustration'
.feature-highlight-popover-sub-content
%p= _('Allows you to add and manage Kubernetes clusters.')
%p
= _('Protip:')
= link_to 'Auto DevOps', help_page_path('topics/autodevops/index.md')
%span= _('uses Kubernetes clusters to deploy your code!')
%hr
%button.btn.btn-create.btn-xs.dismiss-feature-highlight{ type: 'button' }
%span= _("Got it!")
= sprite_icon('thumb-up')
- if @project.feature_available?(:builds, current_user) && !@project.empty_repo?
= nav_link(path: 'pipelines#charts') do
......
......@@ -5,11 +5,11 @@
= s_('ClusterIntegration|Google Kubernetes Engine')
%p
- link_gke = link_to(s_('ClusterIntegration|Google Kubernetes Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Manage your cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke }
= s_('ClusterIntegration|Manage your Kubernetes cluster by visiting %{link_gke}').html_safe % { link_gke: link_gke }
.well.form-group
%label.text-danger
= s_('ClusterIntegration|Remove cluster integration')
= s_('ClusterIntegration|Remove Kubernetes cluster integration')
%p
= s_("ClusterIntegration|Remove this cluster's configuration from this project. This will not delete your actual cluster.")
= link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: s_("ClusterIntegration|Are you sure you want to remove this cluster's integration? This will not delete your actual cluster.")})
= s_("ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster.")
= link_to(s_('ClusterIntegration|Remove integration'), namespace_project_cluster_path(@project.namespace, @project, @cluster.id), method: :delete, class: 'btn btn-danger', data: { confirm: s_("ClusterIntegration|Are you sure you want to remove this Kubernetes cluster's integration? This will not delete your actual Kubernetes cluster.")})
%h4= s_('ClusterIntegration|Cluster integration')
%h4= s_('ClusterIntegration|Kubernetes cluster integration')
.settings-content
.hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' }
= s_('ClusterIntegration|Something went wrong while creating your cluster on Google Kubernetes Engine')
= s_('ClusterIntegration|Something went wrong while creating your Kubernetes cluster on Google Kubernetes Engine')
%p.js-error-reason
.hidden.js-cluster-creating.alert.alert-info.alert-block.append-bottom-10{ role: 'alert' }
= s_('ClusterIntegration|Cluster is being created on Google Kubernetes Engine...')
= s_('ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine...')
.hidden.js-cluster-success.alert.alert-success.alert-block.append-bottom-10{ role: 'alert' }
= s_('ClusterIntegration|Cluster was successfully created on Google Kubernetes Engine. Refresh the page to see cluster\'s details')
= s_("ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details")
%p= s_('ClusterIntegration|Control how your cluster integrates with GitLab')
%p= s_('ClusterIntegration|Control how your Kubernetes cluster integrates with GitLab')
.gl-responsive-table-row
.table-section.section-30
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Cluster")
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Kubernetes cluster")
.table-mobile-content
= link_to cluster.name, namespace_project_cluster_path(@project.namespace, @project, cluster)
.table-section.section-30
......@@ -14,7 +14,7 @@
.table-mobile-content
%button.js-project-feature-toggle.project-feature-toggle{ type: "button",
class: "#{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}",
"aria-label": s_("ClusterIntegration|Toggle Cluster"),
"aria-label": s_("ClusterIntegration|Toggle Kubernetes Cluster"),
disabled: !cluster.can_toggle_cluster?,
data: { endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } }
%input.js-project-feature-toggle-input{ type: "hidden", value: cluster.enabled? }
......
%h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up cluster integration')
%h4.prepend-top-0= s_('ClusterIntegration|Choose how to set up Kubernetes cluster integration')
.dropdown.clusters-dropdown
%button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', data: { toggle: 'dropdown' }, 'aria-haspopup': true, 'aria-expanded': false }
......@@ -7,6 +7,6 @@
= icon('chevron-down')
%ul.dropdown-menu.clusters-dropdown-menu.dropdown-menu-full-width
%li
= link_to(s_('ClusterIntegration|Create cluster on Google Kubernetes Engine'), gcp_new_namespace_project_clusters_path(@project.namespace, @project))
= link_to(s_('ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine'), gcp_new_namespace_project_clusters_path(@project.namespace, @project))
%li
= link_to(s_('ClusterIntegration|Add an existing cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project))
= link_to(s_('ClusterIntegration|Add an existing Kubernetes cluster'), user_new_namespace_project_clusters_path(@project.namespace, @project))
......@@ -3,10 +3,9 @@
.svg-content= image_tag 'illustrations/clusters_empty.svg'
.col-xs-12
.text-content
%h4.text-center= s_('ClusterIntegration|Integrate cluster automation')
- link_to_help_page = link_to(s_('ClusterIntegration|Learn more about Clusters'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
%p= s_('ClusterIntegration|Clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page}
%h4.text-center= s_('ClusterIntegration|Integrate Kubernetes cluster automation')
- link_to_help_page = link_to(s_('ClusterIntegration|Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
%p= s_('ClusterIntegration|Kubernetes clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page}
.text-center
= link_to s_('ClusterIntegration|Add cluster'), new_project_cluster_path(@project), class: 'btn btn-success'
= link_to s_('ClusterIntegration|Add Kubernetes cluster'), new_project_cluster_path(@project), class: 'btn btn-success'
......@@ -5,15 +5,15 @@
%p
- if @cluster.enabled?
- if can?(current_user, :update_cluster, @cluster)
= s_('ClusterIntegration|Cluster integration is enabled for this project. Disabling this integration will not affect your cluster, it will only temporarily turn off GitLab\'s connection to it.')
= s_('ClusterIntegration|Kubernetes cluster integration is enabled for this project. Disabling this integration will not affect your Kubernetes cluster, it will only temporarily turn off GitLab\'s connection to it.')
- else
= s_('ClusterIntegration|Cluster integration is enabled for this project.')
= s_('ClusterIntegration|Kubernetes cluster integration is enabled for this project.')
- else
= s_('ClusterIntegration|Cluster integration is disabled for this project.')
= s_('ClusterIntegration|Kubernetes cluster integration is disabled for this project.')
%label.append-bottom-10.js-cluster-enable-toggle-area
%button{ type: 'button',
class: "js-project-feature-toggle project-feature-toggle #{'is-checked' if @cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}",
"aria-label": s_("ClusterIntegration|Toggle Cluster"),
"aria-label": s_("ClusterIntegration|Toggle Kubernetes cluster"),
disabled: !can?(current_user, :update_cluster, @cluster) }
= field.hidden_field :enabled, { class: 'js-project-feature-toggle-input'}
%span.toggle-icon
......@@ -23,7 +23,7 @@
.form-group
%h5= s_('ClusterIntegration|Environment scope')
%p
= s_("ClusterIntegration|Choose which of your project's environments will use this cluster.")
= s_("ClusterIntegration|Choose which of your project's environments will use this Kubernetes cluster.")
= link_to s_("ClusterIntegration|Learn more about environments"), help_page_path('ci/environments')
= field.text_field :environment_scope, class: 'form-control js-select-on-focus', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
......
%h4.prepend-top-0
= s_('ClusterIntegration|Cluster integration')
= s_('ClusterIntegration|Kubernetes cluster integration')
%p
= s_('ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way.')
= s_('ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way.')
%p
- link = link_to(s_('ClusterIntegration|cluster'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
- link = link_to(_('Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Learn more about %{link_to_documentation}').html_safe % { link_to_documentation: link }
%p
- link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Read our %{link_to_help_page} on cluster integration.').html_safe % { link_to_help_page: link_to_help_page}
= s_('ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration.').html_safe % { link_to_help_page: link_to_help_page}
= form_for @cluster, html: { class: 'prepend-top-20' }, url: gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
= form_errors(@cluster)
.form-group
= field.label :name, s_('ClusterIntegration|Cluster name')
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Cluster name')
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name')
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
.form-group
= field.label :environment_scope, s_('ClusterIntegration|Environment scope')
= field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
......@@ -32,4 +32,4 @@
= provider_gcp_field.text_field :machine_type, class: 'form-control', placeholder: 'n1-standard-4'
.form-group
= field.submit s_('ClusterIntegration|Create cluster'), class: 'btn btn-success'
= field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'btn btn-success'
%h4.prepend-top-20
= s_('ClusterIntegration|Enter the details for your cluster')
= s_('ClusterIntegration|Enter the details for your Kubernetes cluster')
%p
= s_('ClusterIntegration|Please make sure that your Google account meets the following requirements:')
%ul
......@@ -8,7 +8,7 @@
= s_('ClusterIntegration|Your account must have %{link_to_kubernetes_engine}').html_safe % { link_to_kubernetes_engine: link_to_kubernetes_engine }
%li
- link_to_requirements = link_to(s_('ClusterIntegration|meets the requirements'), 'https://cloud.google.com/kubernetes-engine/docs/quickstart?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters').html_safe % { link_to_requirements: link_to_requirements }
= s_('ClusterIntegration|Make sure your account %{link_to_requirements} to create Kubernetes clusters').html_safe % { link_to_requirements: link_to_requirements }
%li
- link_to_container_project = link_to(s_('ClusterIntegration|Google Kubernetes Engine project'), 'https://console.cloud.google.com/home/dashboard?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral', target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below').html_safe % { link_to_container_project: link_to_container_project }
= s_('ClusterIntegration|This account must have permissions to create a Kubernetes cluster in the %{link_to_container_project} specified below').html_safe % { link_to_container_project: link_to_container_project }
.form-group
%label.append-bottom-10{ for: 'cluster-name' }
= s_('ClusterIntegration|Cluster name')
= s_('ClusterIntegration|Kubernetes cluster name')
.input-group
%input.form-control.cluster-name.js-select-on-focus{ value: @cluster.name, readonly: true }
%span.input-group-btn
= clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy cluster name'), class: 'btn-default')
= clipboard_button(text: @cluster.name, title: s_('ClusterIntegration|Copy Kubernetes cluster name'), class: 'btn-default')
= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
= form_errors(@cluster)
......
- breadcrumb_title "Cluster"
- breadcrumb_title 'Kubernetes'
- page_title _("Login")
.row.prepend-top-default
.col-sm-4
= render 'projects/clusters/sidebar'
.col-sm-8
= render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create cluster on Google Kubernetes Engine')
= render 'projects/clusters/dropdown', dropdown_text: s_('ClusterIntegration|Create Kubernetes cluster on Google Kubernetes Engine')
= render 'header'
.row
.col-sm-8.col-sm-offset-4.signin-with-google
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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