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' ...@@ -69,6 +69,10 @@ gem 'net-ldap'
# Git Wiki # Git Wiki
# Required manually in config/initializers/gollum.rb to control load order # 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 gem 'gollum-lib', '~> 4.2', require: false
# Before updating this gem, check if # Before updating this gem, check if
...@@ -407,6 +411,8 @@ end ...@@ -407,6 +411,8 @@ end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.83.0', require: 'gitaly' 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 gem 'toml-rb', '~> 0.3.15', require: false
......
...@@ -340,7 +340,7 @@ GEM ...@@ -340,7 +340,7 @@ GEM
mime-types (~> 3.0) mime-types (~> 3.0)
representable (~> 3.0) representable (~> 3.0)
retriable (>= 2.0, < 4.0) retriable (>= 2.0, < 4.0)
google-protobuf (3.5.1.1) google-protobuf (3.5.1)
googleapis-common-protos-types (1.0.1) googleapis-common-protos-types (1.0.1)
google-protobuf (~> 3.0) google-protobuf (~> 3.0)
googleauth (0.5.3) googleauth (0.5.3)
...@@ -1066,6 +1066,7 @@ DEPENDENCIES ...@@ -1066,6 +1066,7 @@ DEPENDENCIES
gollum-rugged_adapter (~> 0.4.4) gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0) gon (~> 6.1.0)
google-api-client (~> 0.13.6) google-api-client (~> 0.13.6)
google-protobuf (= 3.5.1)
gpgme gpgme
grape (~> 1.0) grape (~> 1.0)
grape-entity (~> 0.6.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"]} {"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 \ 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 @@ ...@@ -2,7 +2,7 @@
import Sortable from 'vendor/Sortable'; import Sortable from 'vendor/Sortable';
import Vue from 'vue'; import Vue from 'vue';
import AccessorUtilities from '../../lib/utils/accessor'; import AccessorUtilities from '../../lib/utils/accessor';
import boardList from './board_list'; import boardList from './board_list.vue';
import boardBlankState from './board_blank_state'; import boardBlankState from './board_blank_state';
import './board_delete'; import './board_delete';
......
<script>
import Sortable from 'vendor/Sortable'; import Sortable from 'vendor/Sortable';
import boardNewIssue from './board_new_issue'; import boardNewIssue from './board_new_issue';
import boardCard from './board_card.vue'; import boardCard from './board_card.vue';
...@@ -8,6 +9,11 @@ const Store = gl.issueBoards.BoardsStore; ...@@ -8,6 +9,11 @@ const Store = gl.issueBoards.BoardsStore;
export default { export default {
name: 'BoardList', name: 'BoardList',
components: {
boardCard,
boardNewIssue,
loadingIcon,
},
props: { props: {
disabled: { disabled: {
type: Boolean, type: Boolean,
...@@ -42,46 +48,6 @@ export default { ...@@ -42,46 +48,6 @@ export default {
showIssueForm: false, 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: { watch: {
filters: { filters: {
handler() { handler() {
...@@ -157,51 +123,90 @@ export default { ...@@ -157,51 +123,90 @@ export default {
eventHub.$off(`scroll-board-list-${this.list.id}`, this.scrollToTop); eventHub.$off(`scroll-board-list-${this.list.id}`, this.scrollToTop);
this.$refs.list.removeEventListener('scroll', this.onScroll); this.$refs.list.removeEventListener('scroll', this.onScroll);
}, },
template: ` methods: {
<div class="board-list-component"> listHeight() {
<div return this.$refs.list.getBoundingClientRect().height;
class="board-list-loading text-center" },
aria-label="Loading issues" scrollHeight() {
v-if="loading"> return this.$refs.list.scrollHeight;
<loading-icon /> },
</div> scrollTop() {
<board-new-issue return this.$refs.list.scrollTop + this.listHeight();
:list="list" },
v-if="list.type !== 'closed' && showIssueForm"/> scrollToTop() {
<ul this.$refs.list.scrollTop = 0;
class="board-list" },
v-show="!loading" loadNextPage() {
ref="list" const getIssues = this.list.nextPage();
:data-board="list.id" const loadingDone = () => {
:class="{ 'is-smaller': showIssueForm }"> this.list.loadingMore = false;
<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 if (getIssues) {
v-show="list.loadingMore" this.list.loadingMore = true;
label="Loading more issues" 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"> <template>
Showing all issues <div class="board-list-component">
</span> <div
<span v-else> class="board-list-loading text-center"
Showing {{ list.issues.length }} of {{ list.issuesSize }} issues aria-label="Loading issues"
</span> v-if="loading">
</li> <loading-icon />
</ul>
</div> </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 { ...@@ -35,10 +35,11 @@ export default class Clusters {
clusterStatus, clusterStatus,
clusterStatusReason, clusterStatusReason,
helpPath, helpPath,
ingressHelpPath,
} = document.querySelector('.js-edit-cluster-form').dataset; } = document.querySelector('.js-edit-cluster-form').dataset;
this.store = new ClustersStore(); this.store = new ClustersStore();
this.store.setHelpPath(helpPath); this.store.setHelpPaths(helpPath, ingressHelpPath);
this.store.updateStatus(clusterStatus); this.store.updateStatus(clusterStatus);
this.store.updateStatusReason(clusterStatusReason); this.store.updateStatusReason(clusterStatusReason);
this.service = new ClustersService({ this.service = new ClustersService({
...@@ -93,6 +94,7 @@ export default class Clusters { ...@@ -93,6 +94,7 @@ export default class Clusters {
props: { props: {
applications: this.state.applications, applications: this.state.applications,
helpPath: this.state.helpPath, helpPath: this.state.helpPath,
ingressHelpPath: this.state.ingressHelpPath,
}, },
}); });
}, },
...@@ -172,7 +174,7 @@ export default class Clusters { ...@@ -172,7 +174,7 @@ export default class Clusters {
.map(appId => newApplicationMap[appId].title); .map(appId => newApplicationMap[appId].title);
if (appTitles.length > 0) { 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(', '), appList: appTitles.join(', '),
}); });
Flash(text, 'notice', this.successApplicationContainer); Flash(text, 'notice', this.successApplicationContainer);
......
...@@ -18,11 +18,16 @@ ...@@ -18,11 +18,16 @@
required: false, required: false,
default: '', default: '',
}, },
ingressHelpPath: {
type: String,
required: false,
default: '',
},
}, },
computed: { computed: {
generalApplicationDescription() { generalApplicationDescription() {
return sprintf( return sprintf(
_.escape(s__(`ClusterIntegration|Install applications on your cluster. _.escape(s__(`ClusterIntegration|Install applications on your Kubernetes cluster.
Read more about %{helpLink}`)), Read more about %{helpLink}`)),
{ {
helpLink: `<a href="${this.helpPath}"> helpLink: `<a href="${this.helpPath}">
...@@ -34,7 +39,7 @@ ...@@ -34,7 +39,7 @@
}, },
helmTillerDescription() { helmTillerDescription() {
return _.escape(s__( 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 Tiller runs inside of your Kubernetes Cluster, and manages
releases of your charts.`, releases of your charts.`,
)); ));
...@@ -49,7 +54,7 @@ ...@@ -49,7 +54,7 @@
_.escape(s__( _.escape(s__(
`ClusterIntegration|%{boldNotice} This will add some extra resources `ClusterIntegration|%{boldNotice} This will add some extra resources
like a load balancer, which may incur additional costs depending on 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}.`, you can %{pricingLink}.`,
)), { )), {
boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`, boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`,
...@@ -59,13 +64,28 @@ ...@@ -59,13 +64,28 @@
false, 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 ` return `
<p> <p>
${descriptionParagraph} ${descriptionParagraph}
</p> </p>
<p class="append-bottom-0"> <p>
${extraCostParagraph} ${extraCostParagraph}
</p> </p>
<p class="settings-message append-bottom-0">
${externalIpParagraph}
</p>
`; `;
}, },
gitlabRunnerDescription() { gitlabRunnerDescription() {
......
...@@ -4,6 +4,7 @@ export default class ClusterStore { ...@@ -4,6 +4,7 @@ export default class ClusterStore {
constructor() { constructor() {
this.state = { this.state = {
helpPath: null, helpPath: null,
ingressHelpPath: null,
status: null, status: null,
statusReason: null, statusReason: null,
applications: { applications: {
...@@ -39,8 +40,9 @@ export default class ClusterStore { ...@@ -39,8 +40,9 @@ export default class ClusterStore {
}; };
} }
setHelpPath(helpPath) { setHelpPaths(helpPath, ingressHelpPath) {
this.state.helpPath = helpPath; this.state.helpPath = helpPath;
this.state.ingressHelpPath = ingressHelpPath;
} }
updateStatus(status) { 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'; ...@@ -26,6 +26,7 @@ import './gl_dropdown';
import initTodoToggle from './header'; import initTodoToggle from './header';
import initImporterStatus from './importer_status'; import initImporterStatus from './importer_status';
import initLayoutNav from './layout_nav'; import initLayoutNav from './layout_nav';
import './feature_highlight/feature_highlight_options';
import LazyLoader from './lazy_loader'; import LazyLoader from './lazy_loader';
import initLogoAnimation from './logo'; import initLogoAnimation from './logo';
import './milestone_select'; import './milestone_select';
......
import _ from 'underscore'; import _ from 'underscore';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { __ } from '~/locale';
export default function initBroadcastMessagesForm() { export default function initBroadcastMessagesForm() {
$('input#broadcast_message_color').on('input', function onMessageColorInput() { $('input#broadcast_message_color').on('input', function onMessageColorInput() {
...@@ -18,13 +21,15 @@ export default function initBroadcastMessagesForm() { ...@@ -18,13 +21,15 @@ export default function initBroadcastMessagesForm() {
if (message === '') { if (message === '') {
$('.js-broadcast-message-preview').text('Your message here'); $('.js-broadcast-message-preview').text('Your message here');
} else { } else {
$.ajax({ axios.post(previewPath, {
url: previewPath, broadcast_message: {
type: 'POST', message,
data: {
broadcast_message: { message },
}, },
}); })
.then(({ data }) => {
$('.js-broadcast-message-preview').html(data.message);
})
.catch(() => flash(__('An error occurred while rendering preview broadcast message')));
} }
}, 250)); }, 250));
} }
import groupAvatar from '~/group_avatar'; 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 */ /* eslint-disable comma-dangle, consistent-return, class-methods-use-this, arrow-parens, no-param-reassign, max-len */
import _ from 'underscore'; import _ from 'underscore';
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { __ } from '~/locale';
const debounceTimeoutDuration = 1000; const debounceTimeoutDuration = 1000;
const invalidInputClass = 'gl-field-error-outline'; const invalidInputClass = 'gl-field-error-outline';
...@@ -77,12 +80,9 @@ export default class UsernameValidator { ...@@ -77,12 +80,9 @@ export default class UsernameValidator {
this.state.pending = true; this.state.pending = true;
this.state.available = false; this.state.available = false;
this.renderState(); this.renderState();
return $.ajax({ axios.get(`${gon.relative_url_root}/users/${username}/exists`)
type: 'GET', .then(({ data }) => this.setAvailabilityState(data.exists))
url: `${gon.relative_url_root}/users/${username}/exists`, .catch(() => flash(__('An error occurred while validating username')));
dataType: 'json',
success: (res) => this.setAvailabilityState(res.exists)
});
} }
} }
......
...@@ -7,6 +7,10 @@ ...@@ -7,6 +7,10 @@
// more than `x` users are referenced. // more than `x` users are referenced.
// //
import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { __ } from '~/locale';
var lastTextareaPreviewed; var lastTextareaPreviewed;
var lastTextareaHeight = null; var lastTextareaHeight = null;
var markdownPreview; var markdownPreview;
...@@ -62,21 +66,17 @@ MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) { ...@@ -62,21 +66,17 @@ MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) {
success(this.ajaxCache.response); success(this.ajaxCache.response);
return; return;
} }
$.ajax({ axios.post(url, {
type: 'POST', text,
url: url, })
data: { .then(({ data }) => {
text: text this.ajaxCache = {
}, text: text,
dataType: 'json', response: data,
success: (function (response) { };
this.ajaxCache = { success(data);
text: text, })
response: response .catch(() => flash(__('An error occurred while fetching markdown preview')));
};
success(response);
}).bind(this)
});
}; };
MarkdownPreview.prototype.hideReferencedUsers = function ($form) { MarkdownPreview.prototype.hideReferencedUsers = function ($form) {
......
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
import flash from './flash';
export default class ProjectLabelSubscription { export default class ProjectLabelSubscription {
constructor(container) { constructor(container) {
this.$container = $(container); this.$container = $(container);
...@@ -17,10 +21,7 @@ export default class ProjectLabelSubscription { ...@@ -17,10 +21,7 @@ export default class ProjectLabelSubscription {
$btn.addClass('disabled'); $btn.addClass('disabled');
$span.toggleClass('hidden'); $span.toggleClass('hidden');
$.ajax({ axios.post(url).then(() => {
type: 'POST',
url,
}).done(() => {
let newStatus; let newStatus;
let newAction; let newAction;
...@@ -45,6 +46,6 @@ export default class ProjectLabelSubscription { ...@@ -45,6 +46,6 @@ export default class ProjectLabelSubscription {
return button; 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 PANEL_STATE from './constants';
import { backOff } from '../lib/utils/common_utils'; import { backOff } from '../lib/utils/common_utils';
...@@ -81,24 +82,20 @@ export default class PrometheusMetrics { ...@@ -81,24 +82,20 @@ export default class PrometheusMetrics {
loadActiveMetrics() { loadActiveMetrics() {
this.showMonitoringMetricsPanelState(PANEL_STATE.LOADING); this.showMonitoringMetricsPanelState(PANEL_STATE.LOADING);
backOff((next, stop) => { backOff((next, stop) => {
$.ajax({ axios.get(this.activeMetricsEndpoint)
url: this.activeMetricsEndpoint, .then(({ data }) => {
dataType: 'json', if (data && data.success) {
global: false, stop(data);
})
.done((res) => {
if (res && res.success) {
stop(res);
} else { } else {
this.backOffRequestCounter = this.backOffRequestCounter += 1; this.backOffRequestCounter = this.backOffRequestCounter += 1;
if (this.backOffRequestCounter < 3) { if (this.backOffRequestCounter < 3) {
next(); next();
} else { } else {
stop(res); stop(data);
} }
} }
}) })
.fail(stop); .catch(stop);
}) })
.then((res) => { .then((res) => {
if (res && res.data && res.data.length) { 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'; import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
export default class ProtectedBranchEdit { export default class ProtectedBranchEdit {
...@@ -38,29 +38,25 @@ export default class ProtectedBranchEdit { ...@@ -38,29 +38,25 @@ export default class ProtectedBranchEdit {
this.$allowedToMergeDropdown.disable(); this.$allowedToMergeDropdown.disable();
this.$allowedToPushDropdown.disable(); this.$allowedToPushDropdown.disable();
$.ajax({ axios.patch(this.$wrap.data('url'), {
type: 'POST', protected_branch: {
url: this.$wrap.data('url'), merge_access_levels_attributes: [{
dataType: 'json', id: this.$allowedToMergeDropdown.data('access-level-id'),
data: { access_level: $allowedToMergeInput.val(),
_method: 'PATCH', }],
protected_branch: { push_access_levels_attributes: [{
merge_access_levels_attributes: [{ id: this.$allowedToPushDropdown.data('access-level-id'),
id: this.$allowedToMergeDropdown.data('access-level-id'), access_level: $allowedToPushInput.val(),
access_level: $allowedToMergeInput.val(), }],
}],
push_access_levels_attributes: [{
id: this.$allowedToPushDropdown.data('access-level-id'),
access_level: $allowedToPushInput.val(),
}],
},
}, },
error() { }).then(() => {
new Flash('Failed to update branch!', 'alert', document.querySelector('.js-protected-branches-list')); this.$allowedToMergeDropdown.enable();
}, this.$allowedToPushDropdown.enable();
}).always(() => { }).catch(() => {
this.$allowedToMergeDropdown.enable(); this.$allowedToMergeDropdown.enable();
this.$allowedToPushDropdown.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'; import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
export default class ProtectedTagEdit { export default class ProtectedTagEdit {
...@@ -28,24 +28,19 @@ export default class ProtectedTagEdit { ...@@ -28,24 +28,19 @@ export default class ProtectedTagEdit {
this.$allowedToCreateDropdownButton.disable(); this.$allowedToCreateDropdownButton.disable();
$.ajax({ axios.patch(this.$wrap.data('url'), {
type: 'POST', protected_tag: {
url: this.$wrap.data('url'), create_access_levels_attributes: [{
dataType: 'json', id: this.$allowedToCreateDropdownButton.data('access-level-id'),
data: { access_level: $allowedToCreateInput.val(),
_method: 'PATCH', }],
protected_tag: {
create_access_levels_attributes: [{
id: this.$allowedToCreateDropdownButton.data('access-level-id'),
access_level: $allowedToCreateInput.val(),
}],
},
}, },
error() { }).then(() => {
new Flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list')); this.$allowedToCreateDropdownButton.enable();
}, }).catch(() => {
}).always(() => {
this.$allowedToCreateDropdownButton.enable(); this.$allowedToCreateDropdownButton.enable();
flash('Failed to update tag!', 'alert', document.querySelector('.js-protected-tags-list'));
}); });
} }
} }
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
import _ from 'underscore'; import _ from 'underscore';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import flash from './flash';
import axios from './lib/utils/axios_utils';
function Sidebar(currentUser) { function Sidebar(currentUser) {
this.toggleTodo = this.toggleTodo.bind(this); this.toggleTodo = this.toggleTodo.bind(this);
...@@ -62,7 +64,7 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) { ...@@ -62,7 +64,7 @@ Sidebar.prototype.sidebarToggleClicked = function (e, triggered) {
Sidebar.prototype.toggleTodo = function(e) { Sidebar.prototype.toggleTodo = function(e) {
var $btnText, $this, $todoLoading, ajaxType, url; var $btnText, $this, $todoLoading, ajaxType, url;
$this = $(e.currentTarget); $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')) { if ($this.attr('data-delete-path')) {
url = "" + ($this.attr('data-delete-path')); url = "" + ($this.attr('data-delete-path'));
} else { } else {
...@@ -71,25 +73,14 @@ Sidebar.prototype.toggleTodo = function(e) { ...@@ -71,25 +73,14 @@ Sidebar.prototype.toggleTodo = function(e) {
$this.tooltip('hide'); $this.tooltip('hide');
return $.ajax({ $('.js-issuable-todo').disable().addClass('is-loading');
url: url,
type: ajaxType, axios[ajaxType](url, {
dataType: 'json', issuable_id: $this.data('issuable-id'),
data: { issuable_type: $this.data('issuable-type'),
issuable_id: $this.data('issuable-id'), }).then(({ data }) => {
issuable_type: $this.data('issuable-type') this.todoUpdateDone(data);
}, }).catch(() => flash(`There was an error ${ajaxType === 'post' ? 'adding a' : 'deleting the'} todo.`));
beforeSend: (function(_this) {
return function() {
$('.js-issuable-todo').disable()
.addClass('is-loading');
};
})(this)
}).done((function(_this) {
return function(data) {
return _this.todoUpdateDone(data);
};
})(this));
}; };
Sidebar.prototype.todoUpdateDone = function(data) { Sidebar.prototype.todoUpdateDone = function(data) {
......
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import Mousetrap from 'mousetrap'; import Mousetrap from 'mousetrap';
import axios from './lib/utils/axios_utils';
import { refreshCurrentPage, visitUrl } from './lib/utils/url_utility'; import { refreshCurrentPage, visitUrl } from './lib/utils/url_utility';
import findAndFollowLink from './shortcuts_dashboard_navigation'; import findAndFollowLink from './shortcuts_dashboard_navigation';
...@@ -85,21 +86,21 @@ export default class Shortcuts { ...@@ -85,21 +86,21 @@ export default class Shortcuts {
$modal.modal('toggle'); $modal.modal('toggle');
} }
$.ajax({ return axios.get(gon.shortcuts_path, {
url: gon.shortcuts_path, responseType: 'text',
dataType: 'script', }).then(({ data }) => {
success() { $.globalEval(data);
if (location && location.length > 0) {
const results = []; if (location && location.length > 0) {
for (let i = 0, len = location.length; i < len; i += 1) { const results = [];
results.push($(location[i]).show()); for (let i = 0, len = location.length; i < len; i += 1) {
} results.push($(location[i]).show());
return results;
} }
return results;
}
$('.hidden-shortcut').show(); $('.hidden-shortcut').show();
return $('.js-more-help-button').remove(); return $('.js-more-help-button').remove();
},
}); });
} }
......
import 'deckar01-task_list'; import 'deckar01-task_list';
import axios from './lib/utils/axios_utils';
import Flash from './flash'; import Flash from './flash';
export default class TaskList { export default class TaskList {
...@@ -7,11 +8,11 @@ export default class TaskList { ...@@ -7,11 +8,11 @@ export default class TaskList {
this.dataType = options.dataType; this.dataType = options.dataType;
this.fieldName = options.fieldName; this.fieldName = options.fieldName;
this.onSuccess = options.onSuccess || (() => {}); this.onSuccess = options.onSuccess || (() => {});
this.onError = function showFlash(response) { this.onError = function showFlash(e) {
let errorMessages = ''; let errorMessages = '';
if (response.responseJSON) { if (e.response.data && typeof e.response.data === 'object') {
errorMessages = response.responseJSON.errors.join(' '); errorMessages = e.response.data.errors.join(' ');
} }
return new Flash(errorMessages || 'Update failed', 'alert'); return new Flash(errorMessages || 'Update failed', 'alert');
...@@ -38,12 +39,9 @@ export default class TaskList { ...@@ -38,12 +39,9 @@ export default class TaskList {
patchData[this.dataType] = { patchData[this.dataType] = {
[this.fieldName]: $target.val(), [this.fieldName]: $target.val(),
}; };
return $.ajax({
type: 'PATCH', return axios.patch($target.data('update-url') || $('form.js-issuable-update').attr('action'), patchData)
url: $target.data('update-url') || $('form.js-issuable-update').attr('action'), .then(({ data }) => this.onSuccess(data))
data: patchData, .catch(err => this.onError(err));
success: this.onSuccess,
error: this.onError,
});
} }
} }
...@@ -8,7 +8,7 @@ import { convertPermissionToBoolean } from './lib/utils/common_utils'; ...@@ -8,7 +8,7 @@ import { convertPermissionToBoolean } from './lib/utils/common_utils';
``` ```
%button.js-project-feature-toggle.project-feature-toggle{ type: "button", %button.js-project-feature-toggle.project-feature-toggle{ type: "button",
class: "#{'is-checked' if enabled?}", 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? } %input{ type: "hidden", class: 'js-project-feature-toggle-input', value: enabled? }
``` ```
*/ */
......
import axios from '../lib/utils/axios_utils';
import Activities from '../activities'; import Activities from '../activities';
import ActivityCalendar from './activity_calendar'; import ActivityCalendar from './activity_calendar';
import { localTimeAgo } from '../lib/utils/datetime_utility'; import { localTimeAgo } from '../lib/utils/datetime_utility';
import { __ } from '../locale';
import flash from '../flash';
/** /**
* UserTabs * UserTabs
...@@ -131,18 +134,20 @@ export default class UserTabs { ...@@ -131,18 +134,20 @@ export default class UserTabs {
} }
loadTab(action, endpoint) { loadTab(action, endpoint) {
return $.ajax({ this.toggleLoading(true);
beforeSend: () => this.toggleLoading(true),
complete: () => this.toggleLoading(false), return axios.get(endpoint)
dataType: 'json', .then(({ data }) => {
url: endpoint,
success: (data) => {
const tabSelector = `div#${action}`; const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html); this.$parentEl.find(tabSelector).html(data.html);
this.loaded[action] = true; this.loaded[action] = true;
localTimeAgo($('.js-timeago', tabSelector)); localTimeAgo($('.js-timeago', tabSelector));
},
}); this.toggleLoading(false);
})
.catch(() => {
this.toggleLoading(false);
});
} }
loadActivities() { loadActivities() {
...@@ -158,17 +163,15 @@ export default class UserTabs { ...@@ -158,17 +163,15 @@ export default class UserTabs {
utcFormatted = `UTC${utcOffset > 0 ? '+' : ''}${(utcOffset / 3600)}`; utcFormatted = `UTC${utcOffset > 0 ? '+' : ''}${(utcOffset / 3600)}`;
} }
$.ajax({ axios.get(calendarPath)
dataType: 'json', .then(({ data }) => {
url: calendarPath,
success: (activityData) => {
$calendarWrap.html(CALENDAR_TEMPLATE); $calendarWrap.html(CALENDAR_TEMPLATE);
$calendarWrap.find('.calendar-hint').append(`(Timezone: ${utcFormatted})`); $calendarWrap.find('.calendar-hint').append(`(Timezone: ${utcFormatted})`);
// eslint-disable-next-line no-new // 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 // eslint-disable-next-line no-new
new Activities(); new Activities();
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
/* global Issuable */ /* global Issuable */
/* global emitSidebarEvent */ /* global emitSidebarEvent */
import _ from 'underscore'; import _ from 'underscore';
import axios from './lib/utils/axios_utils';
// TODO: remove eventHub hack after code splitting refactor // TODO: remove eventHub hack after code splitting refactor
window.emitSidebarEvent = window.emitSidebarEvent || $.noop; window.emitSidebarEvent = window.emitSidebarEvent || $.noop;
...@@ -177,32 +178,28 @@ function UsersSelect(currentUser, els, options = {}) { ...@@ -177,32 +178,28 @@ function UsersSelect(currentUser, els, options = {}) {
$loading.removeClass('hidden').fadeIn(); $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown'); $dropdown.trigger('loading.gl.dropdown');
return $.ajax({ return axios.put(issueURL, data)
type: 'PUT', .then(({ data }) => {
dataType: 'json', var user;
url: issueURL, $dropdown.trigger('loaded.gl.dropdown');
data: data $loading.fadeOut();
}).done(function(data) { if (data.assignee) {
var user; user = {
$dropdown.trigger('loaded.gl.dropdown'); name: data.assignee.name,
$loading.fadeOut(); username: data.assignee.username,
if (data.assignee) { avatar: data.assignee.avatar_url
user = { };
name: data.assignee.name, } else {
username: data.assignee.username, user = {
avatar: data.assignee.avatar_url name: 'Unassigned',
}; username: '',
} else { avatar: ''
user = { };
name: 'Unassigned', }
username: '', $value.html(assigneeTemplate(user));
avatar: '' $collapsedSidebar.attr('title', _.escape(user.name)).tooltip('fixTitle');
}; return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
} });
$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> <% } %>'); 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> <% } %>'); 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) { ...@@ -660,38 +657,33 @@ UsersSelect.prototype.user = function(user_id, callback) {
var url; var url;
url = this.buildUrl(this.userPath); url = this.buildUrl(this.userPath);
url = url.replace(':id', user_id); url = url.replace(':id', user_id);
return $.ajax({ return axios.get(url)
url: url, .then(({ data }) => {
dataType: "json" callback(data);
}).done(function(user) { });
return callback(user);
});
}; };
// Return users list. Filtered by query // Return users list. Filtered by query
// Only active users retrieved // Only active users retrieved
UsersSelect.prototype.users = function(query, options, callback) { UsersSelect.prototype.users = function(query, options, callback) {
var url; const url = this.buildUrl(this.usersPath);
url = this.buildUrl(this.usersPath); const params = {
return $.ajax({ search: query,
url: url, per_page: options.perPage || 20,
data: { active: true,
search: query, project_id: options.projectId || null,
per_page: options.perPage || 20, group_id: options.groupId || null,
active: true, skip_ldap: options.skipLdap || null,
project_id: options.projectId || null, todo_filter: options.todoFilter || null,
group_id: options.groupId || null, todo_state_filter: options.todoStateFilter || null,
skip_ldap: options.skipLdap || null, current_user: options.showCurrentUser || null,
todo_filter: options.todoFilter || null, author_id: options.authorId || null,
todo_state_filter: options.todoStateFilter || null, skip_users: options.skipUsers || null
current_user: options.showCurrentUser || null, };
author_id: options.authorId || null, return axios.get(url, { params })
skip_users: options.skipUsers || null .then(({ data }) => {
}, callback(data);
dataType: "json" });
}).done(function(users) {
return callback(users);
});
}; };
UsersSelect.prototype.buildUrl = function(url) { UsersSelect.prototype.buildUrl = function(url) {
......
...@@ -118,7 +118,7 @@ ...@@ -118,7 +118,7 @@
<template> <template>
<div class="branch-commit"> <div class="branch-commit">
<template v-if="hasCommitRef && showBranch"> <template v-if="hasCommitRef && showBranch">
<div class="icon-container hidden-xs"> <div class="icon-container">
<i <i
v-if="tag" v-if="tag"
class="fa fa-tag" class="fa fa-tag"
...@@ -132,7 +132,7 @@ ...@@ -132,7 +132,7 @@
</div> </div>
<a <a
class="ref-name hidden-xs" class="ref-name"
:href="commitRef.ref_url" :href="commitRef.ref_url"
v-tooltip v-tooltip
data-container="body" data-container="body"
......
...@@ -61,3 +61,4 @@ ...@@ -61,3 +61,4 @@
@import "framework/responsive_tables"; @import "framework/responsive_tables";
@import "framework/stacked-progress-bar"; @import "framework/stacked-progress-bar";
@import "framework/ci_variable_list"; @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 @@ ...@@ -4,6 +4,11 @@
.page-title { .page-title {
margin-top: 0; 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; ...@@ -558,6 +558,7 @@ $jq-ui-default-color: #777;
* Label * Label
*/ */
$label-padding: 7px; $label-padding: 7px;
$label-padding-modal: 10px;
$label-gray-bg: #f8fafc; $label-gray-bg: #f8fafc;
$label-inverse-bg: #333; $label-inverse-bg: #333;
$label-remove-border: rgba(0, 0, 0, .1); $label-remove-border: rgba(0, 0, 0, .1);
......
...@@ -224,3 +224,16 @@ ...@@ -224,3 +224,16 @@
border-radius: $label-border-radius; border-radius: $label-border-radius;
font-weight: $gl-font-weight-normal; 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 @@ ...@@ -58,13 +58,13 @@
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
width: 200px; width: 200px;
margin-left: $gl-padding * 2;
margin-bottom: 0; margin-bottom: 0;
} }
.label { .label {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
vertical-align: middle;
max-width: 100%; max-width: 100%;
} }
} }
...@@ -79,26 +79,33 @@ ...@@ -79,26 +79,33 @@
width: 100px; width: 100px;
margin-left: 10px; margin-left: 10px;
margin-bottom: 0; margin-bottom: 0;
vertical-align: middle; vertical-align: top;
} }
} }
.label-description { .label-description {
display: block; display: block;
margin-bottom: 10px; margin-bottom: 10px;
margin-left: 50px;
.description-text {
margin-bottom: $gl-padding;
}
a {
color: $blue-600;
}
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
display: inline-block; display: inline-block;
width: 30%; max-width: 50%;
margin-left: 10px; margin-left: 10px;
margin-bottom: 0; margin-bottom: 0;
vertical-align: middle; vertical-align: top;
} }
} }
.label { .label {
padding: 8px 9px 9px; padding: 8px 12px;
font-size: 14px; font-size: 14px;
} }
} }
...@@ -116,6 +123,12 @@ ...@@ -116,6 +123,12 @@
} }
.manage-labels-list { .manage-labels-list {
@media(min-width: $screen-md-min) {
&.content-list li {
padding: $gl-padding 0;
}
}
> li:not(.empty-message):not(.is-not-draggable) { > li:not(.empty-message):not(.is-not-draggable) {
background-color: $white-light; background-color: $white-light;
cursor: move; cursor: move;
...@@ -133,8 +146,6 @@ ...@@ -133,8 +146,6 @@
} }
.btn-action { .btn-action {
color: $gl-text-color;
.fa { .fa {
font-size: 18px; font-size: 18px;
vertical-align: middle; vertical-align: middle;
...@@ -155,10 +166,18 @@ ...@@ -155,10 +166,18 @@
float: right; float: right;
} }
} }
@media (max-width: $screen-xs-max) {
.dropdown-menu {
min-width: 100%;
}
}
} }
.draggable-handler { .draggable-handler {
display: inline-block; display: inline-block;
vertical-align: top;
margin: 5px 0;
opacity: 0; opacity: 0;
transition: opacity .3s; transition: opacity .3s;
color: $gray-darkest; color: $gray-darkest;
...@@ -188,7 +207,7 @@ ...@@ -188,7 +207,7 @@
.toggle-priority { .toggle-priority {
display: inline-block; display: inline-block;
vertical-align: middle; vertical-align: top;
button { button {
border-color: transparent; border-color: transparent;
...@@ -255,6 +274,11 @@ ...@@ -255,6 +274,11 @@
} }
.label-subscribe-button { .label-subscribe-button {
@media(min-width: $screen-md-min) {
min-width: 105px;
margin-left: $gl-padding;
}
.label-subscribe-button-icon { .label-subscribe-button-icon {
&[disabled] { &[disabled] {
opacity: 0.5; opacity: 0.5;
......
...@@ -121,7 +121,7 @@ ...@@ -121,7 +121,7 @@
.ref-name { .ref-name {
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
max-width: 120px; max-width: 100px;
overflow: hidden; overflow: hidden;
display: inline-block; display: inline-block;
white-space: nowrap; white-space: nowrap;
......
...@@ -6,6 +6,14 @@ ...@@ -6,6 +6,14 @@
} }
} }
.wiki-form {
.edit-wiki-page-slug-tip {
display: inline-block;
max-width: 100%;
margin-top: 5px;
}
}
.title .edit-wiki-header { .title .edit-wiki-header {
width: 780px; width: 780px;
margin-left: auto; margin-left: auto;
......
class Admin::BroadcastMessagesController < Admin::ApplicationController class Admin::BroadcastMessagesController < Admin::ApplicationController
include BroadcastMessagesHelper
before_action :finder, only: [:edit, :update, :destroy] before_action :finder, only: [:edit, :update, :destroy]
def index def index
...@@ -37,7 +39,8 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController ...@@ -37,7 +39,8 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
end end
def preview 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 end
protected protected
......
...@@ -70,7 +70,7 @@ module UploadsActions ...@@ -70,7 +70,7 @@ module UploadsActions
end end
def build_uploader_from_params 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.retrieve_from_store!(params[:filename])
uploader uploader
end end
......
...@@ -54,7 +54,7 @@ class Groups::LabelsController < Groups::ApplicationController ...@@ -54,7 +54,7 @@ class Groups::LabelsController < Groups::ApplicationController
respond_to do |format| respond_to do |format|
format.html do 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 end
format.js format.js
end end
......
...@@ -10,7 +10,7 @@ class GroupsController < Groups::ApplicationController ...@@ -10,7 +10,7 @@ class GroupsController < Groups::ApplicationController
before_action :group, except: [:index, :new, :create] before_action :group, except: [:index, :new, :create]
# Authorize # 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 :authorize_create_group!, only: [:new]
before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests] before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests]
...@@ -94,6 +94,19 @@ class GroupsController < Groups::ApplicationController ...@@ -94,6 +94,19 @@ class GroupsController < Groups::ApplicationController
redirect_to root_path, status: 302, alert: "Group '#{@group.name}' was scheduled for deletion." redirect_to root_path, status: 302, alert: "Group '#{@group.name}' was scheduled for deletion."
end 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 protected
def authorize_create_group! def authorize_create_group!
......
...@@ -42,7 +42,7 @@ class Projects::Clusters::GcpController < Projects::ApplicationController ...@@ -42,7 +42,7 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
when 'true' when 'true'
return return
when 'false' 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 else
flash[:alert] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.') flash[:alert] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.')
end end
......
...@@ -41,7 +41,7 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -41,7 +41,7 @@ class Projects::ClustersController < Projects::ApplicationController
head :no_content head :no_content
end end
format.html do format.html do
flash[:notice] = "Cluster was successfully updated." flash[:notice] = _('Kubernetes cluster was successfully updated.')
redirect_to project_cluster_path(project, cluster) redirect_to project_cluster_path(project, cluster)
end end
end end
...@@ -55,10 +55,10 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -55,10 +55,10 @@ class Projects::ClustersController < Projects::ApplicationController
def destroy def destroy
if cluster.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 redirect_to project_clusters_path(project), status: 302
else else
flash[:notice] = "Cluster integration was not removed." flash[:notice] = _('Kubernetes cluster integration was not removed.')
render :show render :show
end end
end end
......
...@@ -5,6 +5,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController ...@@ -5,6 +5,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
rescue_from Gitlab::GitAccess::UnauthorizedError, with: :render_403 rescue_from Gitlab::GitAccess::UnauthorizedError, with: :render_403
rescue_from Gitlab::GitAccess::NotFoundError, with: :render_404 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-upload-pack (git pull)
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push) # GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
...@@ -55,8 +56,15 @@ class Projects::GitHttpController < Projects::GitHttpClientController ...@@ -55,8 +56,15 @@ class Projects::GitHttpController < Projects::GitHttpClientController
render plain: exception.message, status: :not_found render plain: exception.message, status: :not_found
end end
def render_422(exception)
render plain: exception.message, status: :unprocessable_entity
end
def access 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 end
def access_actor def access_actor
...@@ -68,12 +76,17 @@ class Projects::GitHttpController < Projects::GitHttpClientController ...@@ -68,12 +76,17 @@ class Projects::GitHttpController < Projects::GitHttpClientController
# Use the magic string '_any' to indicate we do not know what the # Use the magic string '_any' to indicate we do not know what the
# changes are. This is also what gitlab-shell does. # changes are. This is also what gitlab-shell does.
access.check(git_command, '_any') access.check(git_command, '_any')
@project ||= access.project
end end
def access_klass def access_klass
@access_klass ||= wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess @access_klass ||= wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess
end end
def project_path
@project_path ||= params[:project_id].sub(/\.git$/, '')
end
def log_user_activity def log_user_activity
Users::ActivityService.new(user, 'pull').execute Users::ActivityService.new(user, 'pull').execute
end end
......
...@@ -54,8 +54,8 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -54,8 +54,8 @@ class Projects::WikisController < Projects::ApplicationController
else else
render 'edit' render 'edit'
end end
rescue WikiPage::PageChangedError rescue WikiPage::PageChangedError, WikiPage::PageRenameError => e
@conflict = true @error = e
render 'edit' render 'edit'
end end
...@@ -76,9 +76,9 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -76,9 +76,9 @@ class Projects::WikisController < Projects::ApplicationController
@page = @project_wiki.find_page(params[:id]) @page = @project_wiki.find_page(params[:id])
if @page 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) total_count: @page.count_versions)
.page(params[:page]) .page(params[:page])
else else
redirect_to( redirect_to(
project_wiki_path(@project, :home), 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 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) def can_change_group_visibility_level?(group)
can?(current_user, :change_visibility_level, group) can?(current_user, :change_visibility_level, group)
end end
...@@ -88,6 +92,19 @@ module GroupsHelper ...@@ -88,6 +92,19 @@ module GroupsHelper
end end
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 private
def group_title_link(group, hidable: false, show_avatar: false, for_dropdown: false) 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 ...@@ -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 add_to_breadcrumb_dropdown link_to(WikiPage.unhyphenize(dir_or_page).capitalize, project_wiki_path(@project, current_slug)), location: :after
end end
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 end
...@@ -21,6 +21,7 @@ module Ci ...@@ -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_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_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_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 # The "environment" field for builds is a String, and is the unexpanded name
def persisted_environment def persisted_environment
......
...@@ -9,9 +9,12 @@ module Ci ...@@ -9,9 +9,12 @@ module Ci
mount_uploader :file, JobArtifactUploader mount_uploader :file, JobArtifactUploader
delegate :open, :exists?, to: :file
enum file_type: { enum file_type: {
archive: 1, archive: 1,
metadata: 2 metadata: 2,
trace: 3
} }
def self.artifacts_size_for(project) def self.artifacts_size_for(project)
......
...@@ -180,7 +180,7 @@ module Clusters ...@@ -180,7 +180,7 @@ module Clusters
return unless managed? return unless managed?
if api_url_changed? || token_changed? || ca_pem_changed? 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 return false
end end
......
...@@ -39,7 +39,6 @@ module ArtifactMigratable ...@@ -39,7 +39,6 @@ module ArtifactMigratable
end end
def artifacts_size def artifacts_size
read_attribute(:artifacts_size).to_i + read_attribute(:artifacts_size).to_i + job_artifacts.sum(:size).to_i
job_artifacts_archive&.size.to_i + job_artifacts_metadata&.size.to_i
end end
end end
...@@ -7,11 +7,12 @@ module Routable ...@@ -7,11 +7,12 @@ module Routable
has_one :route, as: :source, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent 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 has_many :redirect_routes, as: :source, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
validates_associated :route
validates :route, presence: true validates :route, presence: true
scope :with_route, -> { includes(:route) } scope :with_route, -> { includes(:route) }
after_validation :set_path_errors
before_validation do before_validation do
if full_path_changed? || full_name_changed? if full_path_changed? || full_name_changed?
prepare_route prepare_route
...@@ -125,6 +126,11 @@ module Routable ...@@ -125,6 +126,11 @@ module Routable
private 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 def uncached_full_path
if route && route.path.present? if route && route.path.present?
@full_path ||= route.path # rubocop:disable Gitlab/ModuleWithInstanceVariables @full_path ||= route.path # rubocop:disable Gitlab/ModuleWithInstanceVariables
......
...@@ -14,7 +14,11 @@ module Storage ...@@ -14,7 +14,11 @@ module Storage
# Ensure old directory exists before moving it # Ensure old directory exists before moving it
gitlab_shell.add_namespace(repository_storage_path, full_path_was) 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) 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}" 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 # if we cannot move namespace directory we should rollback
......
...@@ -20,6 +20,9 @@ class Namespace < ActiveRecord::Base ...@@ -20,6 +20,9 @@ class Namespace < ActiveRecord::Base
has_many :projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :projects, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :project_statistics 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 :owner, class_name: "User"
belongs_to :parent, class_name: "Namespace" belongs_to :parent, class_name: "Namespace"
...@@ -29,7 +32,6 @@ class Namespace < ActiveRecord::Base ...@@ -29,7 +32,6 @@ class Namespace < ActiveRecord::Base
validates :owner, presence: true, unless: ->(n) { n.type == "Group" } validates :owner, presence: true, unless: ->(n) { n.type == "Group" }
validates :name, validates :name,
presence: true, presence: true,
uniqueness: { scope: :parent_id },
length: { maximum: 255 }, length: { maximum: 255 },
namespace_name: true namespace_name: true
...@@ -40,7 +42,6 @@ class Namespace < ActiveRecord::Base ...@@ -40,7 +42,6 @@ class Namespace < ActiveRecord::Base
namespace_path: true namespace_path: true
validate :nesting_level_allowed validate :nesting_level_allowed
validate :allowed_path_by_redirects
delegate :name, to: :owner, allow_nil: true, prefix: true delegate :name, to: :owner, allow_nil: true, prefix: true
...@@ -52,7 +53,7 @@ class Namespace < ActiveRecord::Base ...@@ -52,7 +53,7 @@ class Namespace < ActiveRecord::Base
# Legacy Storage specific hooks # 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 } before_destroy(prepend: true) { prepare_for_destroy }
after_destroy :rm_dir after_destroy :rm_dir
...@@ -222,9 +223,12 @@ class Namespace < ActiveRecord::Base ...@@ -222,9 +223,12 @@ class Namespace < ActiveRecord::Base
end end
def full_path_was def full_path_was
return path_was unless has_parent? if parent_id_was.nil?
path_was
"#{parent.full_path}/#{path_was}" else
previous_parent = Group.find_by(id: parent_id_was)
previous_parent.full_path + '/' + path_was
end
end end
# Exports belonging to projects with legacy storage are placed in a common # Exports belonging to projects with legacy storage are placed in a common
...@@ -241,6 +245,10 @@ class Namespace < ActiveRecord::Base ...@@ -241,6 +245,10 @@ class Namespace < ActiveRecord::Base
private private
def path_or_parent_changed?
path_changed? || parent_changed?
end
def refresh_access_of_projects_invited_groups def refresh_access_of_projects_invited_groups
Group Group
.joins(project_group_links: :project) .joins(project_group_links: :project)
...@@ -271,16 +279,6 @@ class Namespace < ActiveRecord::Base ...@@ -271,16 +279,6 @@ class Namespace < ActiveRecord::Base
.update_all(share_with_group_lock: true) .update_all(share_with_group_lock: true)
end 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 def write_projects_repository_config
all_projects.find_each do |project| all_projects.find_each do |project|
project.expires_full_path_cache # we need to clear cache to validate renames correctly project.expires_full_path_cache # we need to clear cache to validate renames correctly
......
...@@ -60,7 +60,7 @@ class Note < ActiveRecord::Base ...@@ -60,7 +60,7 @@ class Note < ActiveRecord::Base
belongs_to :updated_by, class_name: "User" belongs_to :updated_by, class_name: "User"
belongs_to :last_edited_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_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :system_note_metadata has_one :system_note_metadata
......
...@@ -245,8 +245,7 @@ class Project < ActiveRecord::Base ...@@ -245,8 +245,7 @@ class Project < ActiveRecord::Base
validates :path, validates :path,
presence: true, presence: true,
project_path: true, project_path: true,
length: { maximum: 255 }, length: { maximum: 255 }
uniqueness: { scope: :namespace_id }
validates :namespace, presence: true validates :namespace, presence: true
validates :name, uniqueness: { scope: :namespace_id } validates :name, uniqueness: { scope: :namespace_id }
...@@ -511,10 +510,13 @@ class Project < ActiveRecord::Base ...@@ -511,10 +510,13 @@ class Project < ActiveRecord::Base
@repository ||= Repository.new(full_path, self, disk_path: disk_path) @repository ||= Repository.new(full_path, self, disk_path: disk_path)
end end
def reload_repository! def cleanup
@repository&.cleanup
@repository = nil @repository = nil
end end
alias_method :reload_repository!, :cleanup
def container_registry_url def container_registry_url
if Gitlab.config.registry.enabled if Gitlab.config.registry.enabled
"#{Gitlab.config.registry.host_port}/#{full_path.downcase}" "#{Gitlab.config.registry.host_port}/#{full_path.downcase}"
......
...@@ -150,9 +150,10 @@ class KubernetesService < DeploymentService ...@@ -150,9 +150,10 @@ class KubernetesService < DeploymentService
end end
def deprecation_message def deprecation_message
content = <<-MESSAGE.strip_heredoc content = _("Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page") % {
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 deprecated_message_content: deprecated_message_content,
MESSAGE url: Gitlab::Routing.url_helpers.project_clusters_path(project)
}
content.html_safe content.html_safe
end end
...@@ -248,9 +249,9 @@ class KubernetesService < DeploymentService ...@@ -248,9 +249,9 @@ class KubernetesService < DeploymentService
def deprecated_message_content def deprecated_message_content
if active? 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 else
"Fields on this page are now uneditable, you can configure" _("Fields on this page are now uneditable, you can configure")
end end
end end
end end
...@@ -119,6 +119,8 @@ class ProjectWiki ...@@ -119,6 +119,8 @@ class ProjectWiki
end end
def delete_page(page, message = nil) def delete_page(page, message = nil)
return unless page
wiki.delete_page(page.path, commit_details(:deleted, message, page.title)) wiki.delete_page(page.path, commit_details(:deleted, message, page.title))
update_project_activity update_project_activity
...@@ -131,6 +133,8 @@ class ProjectWiki ...@@ -131,6 +133,8 @@ class ProjectWiki
end end
def page_title_and_dir(title) def page_title_and_dir(title)
return unless title
title_array = title.split("/") title_array = title.split("/")
title = title_array.pop title = title_array.pop
[title, title_array.join("/")] [title, title_array.join("/")]
......
...@@ -93,6 +93,10 @@ class Repository ...@@ -93,6 +93,10 @@ class Repository
alias_method :raw, :raw_repository alias_method :raw, :raw_repository
def cleanup
@raw_repository&.cleanup
end
# Return absolute path to repository # Return absolute path to repository
def path_to_repo def path_to_repo
@path_to_repo ||= File.expand_path( @path_to_repo ||= File.expand_path(
......
...@@ -75,7 +75,7 @@ class Route < ActiveRecord::Base ...@@ -75,7 +75,7 @@ class Route < ActiveRecord::Base
def ensure_permanent_paths def ensure_permanent_paths
return if path.nil? 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 end
def conflicting_redirect_exists? def conflicting_redirect_exists?
......
...@@ -28,6 +28,7 @@ class Todo < ActiveRecord::Base ...@@ -28,6 +28,7 @@ class Todo < ActiveRecord::Base
delegate :name, :email, to: :author, prefix: true, allow_nil: true delegate :name, :email, to: :author, prefix: true, allow_nil: true
validates :action, :project, :target_type, :user, presence: true validates :action, :project, :target_type, :user, presence: true
validates :author, presence: true
validates :target_id, presence: true, unless: :for_commit? validates :target_id, presence: true, unless: :for_commit?
validates :commit_id, presence: true, if: :for_commit? validates :commit_id, presence: true, if: :for_commit?
......
...@@ -12,6 +12,10 @@ class Upload < ActiveRecord::Base ...@@ -12,6 +12,10 @@ class Upload < ActiveRecord::Base
before_save :calculate_checksum!, if: :foreground_checksummable? before_save :calculate_checksum!, if: :foreground_checksummable?
after_commit :schedule_checksum, if: :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) def self.hexdigest(path)
Digest::SHA256.file(path).hexdigest Digest::SHA256.file(path).hexdigest
end end
...@@ -30,7 +34,7 @@ class Upload < ActiveRecord::Base ...@@ -30,7 +34,7 @@ class Upload < ActiveRecord::Base
end end
def build_uploader 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.upload = self
uploader.retrieve_from_store!(identifier) uploader.retrieve_from_store!(identifier)
end end
...@@ -40,8 +44,19 @@ class Upload < ActiveRecord::Base ...@@ -40,8 +44,19 @@ class Upload < ActiveRecord::Base
File.exist?(absolute_path) File.exist?(absolute_path)
end end
def uploader_context
{
identifier: identifier,
secret: secret
}.compact
end
private private
def delete_file!
build_uploader.remove!
end
def checksummable? def checksummable?
checksum.nil? && local? && exist? checksum.nil? && local? && exist?
end end
...@@ -62,11 +77,15 @@ class Upload < ActiveRecord::Base ...@@ -62,11 +77,15 @@ class Upload < ActiveRecord::Base
!path.start_with?('/') !path.start_with?('/')
end end
def uploader_class
Object.const_get(uploader)
end
def identifier def identifier
File.basename(path) File.basename(path)
end end
def uploader_class def mount_point
Object.const_get(uploader) super&.to_sym
end end
end end
...@@ -77,7 +77,7 @@ class User < ActiveRecord::Base ...@@ -77,7 +77,7 @@ class User < ActiveRecord::Base
# #
# Namespace for personal projects # 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 # Profile
has_many :keys, -> do has_many :keys, -> do
...@@ -125,7 +125,7 @@ class User < ActiveRecord::Base ...@@ -125,7 +125,7 @@ class User < ActiveRecord::Base
has_many :spam_logs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent 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 :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 :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 :notification_settings, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :award_emoji, 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 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 ...@@ -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 :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 :custom_attributes, class_name: 'UserCustomAttribute'
has_many :callouts, class_name: 'UserCallout'
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
# #
...@@ -150,12 +151,9 @@ class User < ActiveRecord::Base ...@@ -150,12 +151,9 @@ class User < ActiveRecord::Base
validates :projects_limit, validates :projects_limit,
presence: true, presence: true,
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE } numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
validates :username, validates :username, presence: true
user_path: true,
presence: true,
uniqueness: { case_sensitive: false }
validate :namespace_uniq, if: :username_changed? validates :namespace, presence: true
validate :namespace_move_dir_allowed, if: :username_changed? validate :namespace_move_dir_allowed, if: :username_changed?
validate :unique_email, if: :email_changed? validate :unique_email, if: :email_changed?
...@@ -170,7 +168,8 @@ class User < ActiveRecord::Base ...@@ -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 :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 :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? } 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_update :username_changed_hook, if: :username_changed?
after_destroy :post_destroy_hook after_destroy :post_destroy_hook
after_destroy :remove_key_cache after_destroy :remove_key_cache
...@@ -229,8 +228,8 @@ class User < ActiveRecord::Base ...@@ -229,8 +228,8 @@ class User < ActiveRecord::Base
scope :active, -> { with_state(:active).non_internal } 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 :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 :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_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('last_sign_in_at', 'ASC')) } scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
def self.with_two_factor def self.with_two_factor
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id") joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id")
...@@ -504,17 +503,6 @@ class User < ActiveRecord::Base ...@@ -504,17 +503,6 @@ class User < ActiveRecord::Base
end end
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 def namespace_move_dir_allowed
if namespace&.any_project_has_container_registry_tags? if namespace&.any_project_has_container_registry_tags?
errors.add(:username, 'cannot be changed if a personal 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 ...@@ -883,19 +871,18 @@ class User < ActiveRecord::Base
end end
def ensure_namespace_correct def ensure_namespace_correct
# Ensure user has namespace if namespace
create_namespace!(path: username, name: username) unless namespace namespace.path = namespace.name = username if username_changed?
else
if username_changed? build_namespace(path: username, name: username)
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
end end
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 def username_changed_hook
system_hook_service.execute_hooks_for(self, :rename) system_hook_service.execute_hooks_for(self, :rename)
end 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 class WikiPage
PageChangedError = Class.new(StandardError) PageChangedError = Class.new(StandardError)
PageRenameError = Class.new(StandardError)
include ActiveModel::Validations include ActiveModel::Validations
include ActiveModel::Conversion include ActiveModel::Conversion
...@@ -102,7 +103,7 @@ class WikiPage ...@@ -102,7 +103,7 @@ class WikiPage
# The hierarchy of the directory this page is contained in. # The hierarchy of the directory this page is contained in.
def directory def directory
wiki.page_title_and_dir(slug).last wiki.page_title_and_dir(slug)&.last.to_s
end end
# The processed/formatted content of this page. # The processed/formatted content of this page.
...@@ -177,7 +178,7 @@ class WikiPage ...@@ -177,7 +178,7 @@ class WikiPage
# Creates a new Wiki Page. # Creates a new Wiki Page.
# #
# attr - Hash of attributes to set on the new 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. # :content - The raw markup content.
# :format - Optional symbol representing the # :format - Optional symbol representing the
# content format. Can be any type # content format. Can be any type
...@@ -189,7 +190,7 @@ class WikiPage ...@@ -189,7 +190,7 @@ class WikiPage
# Returns the String SHA1 of the newly created page # Returns the String SHA1 of the newly created page
# or False if the save was unsuccessful. # or False if the save was unsuccessful.
def create(attrs = {}) def create(attrs = {})
@attributes.merge!(attrs) update_attributes(attrs)
save(page_details: title) do save(page_details: title) do
wiki.create_page(title, content, format, message) wiki.create_page(title, content, format, message)
...@@ -204,24 +205,29 @@ class WikiPage ...@@ -204,24 +205,29 @@ class WikiPage
# See ProjectWiki::MARKUPS Hash for available formats. # See ProjectWiki::MARKUPS Hash for available formats.
# :message - Optional commit message to set on the new version. # :message - Optional commit message to set on the new version.
# :last_commit_sha - Optional last commit sha to validate the page unchanged. # :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 # Returns the String SHA1 of the newly created page
# or False if the save was unsuccessful. # or False if the save was unsuccessful.
def update(attrs = {}) def update(attrs = {})
last_commit_sha = attrs.delete(:last_commit_sha) last_commit_sha = attrs.delete(:last_commit_sha)
if last_commit_sha && last_commit_sha != self.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 end
attrs.slice!(:content, :format, :message, :title) update_attributes(attrs)
@attributes.merge!(attrs)
page_details = if title_changed?
if title.present? && @page.title != title page_details = title
title
else if wiki.find_page(page_details).present?
@page.url_path @attributes[:title] = @page.url_path
raise PageRenameError
end end
else
page_details = @page.url_path
end
save(page_details: page_details) do save(page_details: page_details) do
wiki.update_page( wiki.update_page(
...@@ -255,8 +261,44 @@ class WikiPage ...@@ -255,8 +261,44 @@ class WikiPage
page.version.to_s page.version.to_s
end end
def title_changed?
title.present? && self.class.unhyphenize(@page.url_path) != title
end
private 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 def set_attributes
attributes[:slug] = @page.url_path attributes[:slug] = @page.url_path
attributes[:title] = @page.title 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 ...@@ -5,7 +5,7 @@ module Clusters
def execute(access_token = nil) def execute(access_token = nil)
@access_token = access_token @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| create_cluster.tap do |cluster|
ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted? ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted?
......
...@@ -28,7 +28,7 @@ module Clusters ...@@ -28,7 +28,7 @@ module Clusters
if elapsed_time_from_creation(operation) < TIMEOUT if elapsed_time_from_creation(operation) < TIMEOUT
WaitForClusterCreationWorker.perform_in(EAGER_INTERVAL, provider.cluster_id) WaitForClusterCreationWorker.perform_in(EAGER_INTERVAL, provider.cluster_id)
else else
provider.make_errored!("Cluster creation time exceeds timeout; #{TIMEOUT}") provider.make_errored!(_('Kubernetes cluster creation time exceeds timeout; %{timeout}') % { timeout: TIMEOUT })
end end
end end
......
module Files module Files
class CreateService < Files::BaseService class CreateService < Files::BaseService
def create_commit! 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( repository.create_file(
current_user, current_user,
@file_path, @file_path,
@file_content, content_or_lfs_pointer,
message: @commit_message, message: @commit_message,
branch_name: @branch_name, branch_name: @branch_name,
author_email: @author_email, 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 ...@@ -49,11 +49,11 @@ class FileMover
end end
def uploader def uploader
@uploader ||= PersonalFileUploader.new(model, secret) @uploader ||= PersonalFileUploader.new(model, secret: secret)
end end
def temp_file_uploader def temp_file_uploader
@temp_file_uploader ||= PersonalFileUploader.new(nil, secret) @temp_file_uploader ||= PersonalFileUploader.new(nil, secret: secret)
end end
def revert def revert
......
...@@ -15,6 +15,8 @@ class FileUploader < GitlabUploader ...@@ -15,6 +15,8 @@ class FileUploader < GitlabUploader
storage :file storage :file
after :remove, :prune_store_dir
def self.root def self.root
File.join(options.storage_path, 'uploads') File.join(options.storage_path, 'uploads')
end end
...@@ -62,9 +64,11 @@ class FileUploader < GitlabUploader ...@@ -62,9 +64,11 @@ class FileUploader < GitlabUploader
attr_accessor :model attr_accessor :model
def initialize(model, secret = nil) def initialize(model, mounted_as = nil, **uploader_context)
super(model, nil, **uploader_context)
@model = model @model = model
@secret = secret apply_context!(uploader_context)
end end
def base_dir def base_dir
...@@ -107,15 +111,17 @@ class FileUploader < GitlabUploader ...@@ -107,15 +111,17 @@ class FileUploader < GitlabUploader
self.file.filename self.file.filename
end end
# the upload does not hold the secret, but holds the path
# which contains the secret: extract it
def upload=(value) 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) if matches = DYNAMIC_PATH_PATTERN.match(value.path)
@secret = matches[:secret] @secret = matches[:secret]
@identifier = matches[:identifier] @identifier = matches[:identifier]
end end
super
end end
def secret def secret
...@@ -124,6 +130,22 @@ class FileUploader < GitlabUploader ...@@ -124,6 +130,22 @@ class FileUploader < GitlabUploader
private 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 def markdown_name
(image_or_video? ? File.basename(filename, File.extname(filename)) : filename).gsub("]", "\\]") (image_or_video? ? File.basename(filename, File.extname(filename)) : filename).gsub("]", "\\]")
end end
......
...@@ -29,6 +29,10 @@ class GitlabUploader < CarrierWave::Uploader::Base ...@@ -29,6 +29,10 @@ class GitlabUploader < CarrierWave::Uploader::Base
delegate :base_dir, :file_storage?, to: :class delegate :base_dir, :file_storage?, to: :class
def initialize(model, mounted_as = nil, **uploader_context)
super(model, mounted_as)
end
def file_cache_storage? def file_cache_storage?
cache_storage.is_a?(CarrierWave::Storage::File) cache_storage.is_a?(CarrierWave::Storage::File)
end end
......
...@@ -13,6 +13,12 @@ class JobArtifactUploader < GitlabUploader ...@@ -13,6 +13,12 @@ class JobArtifactUploader < GitlabUploader
dynamic_segment dynamic_segment
end end
def open
raise 'Only File System is supported' unless file_storage?
File.open(path, "rb") if path
end
private private
def dynamic_segment def dynamic_segment
......
...@@ -24,7 +24,7 @@ module RecordsUploads ...@@ -24,7 +24,7 @@ module RecordsUploads
uploads.where(path: upload_path).delete_all uploads.where(path: upload_path).delete_all
upload.destroy! if upload upload.destroy! if upload
self.upload = build_upload_from_uploader(self) self.upload = build_upload
upload.save! upload.save!
end end
end end
...@@ -39,12 +39,13 @@ module RecordsUploads ...@@ -39,12 +39,13 @@ module RecordsUploads
Upload.order(id: :desc).where(uploader: self.class.to_s) Upload.order(id: :desc).where(uploader: self.class.to_s)
end end
def build_upload_from_uploader(uploader) def build_upload
Upload.new( Upload.new(
size: uploader.file.size, uploader: self.class.to_s,
path: uploader.upload_path, size: file.size,
model: uploader.model, path: upload_path,
uploader: uploader.class.to_s model: model,
mount_point: mounted_as
) )
end end
......
...@@ -13,10 +13,6 @@ class AbstractPathValidator < ActiveModel::EachValidator ...@@ -13,10 +13,6 @@ class AbstractPathValidator < ActiveModel::EachValidator
raise NotImplementedError raise NotImplementedError
end end
def self.full_path(record, value)
value
end
def self.valid_path?(path) def self.valid_path?(path)
encode!(path) encode!(path)
"#{path}/" =~ path_regex "#{path}/" =~ path_regex
...@@ -28,7 +24,7 @@ class AbstractPathValidator < ActiveModel::EachValidator ...@@ -28,7 +24,7 @@ class AbstractPathValidator < ActiveModel::EachValidator
return return
end end
full_path = self.class.full_path(record, value) full_path = record.build_full_path
return unless full_path return unless full_path
unless self.class.valid_path?(full_path) unless self.class.valid_path?(full_path)
......
...@@ -12,8 +12,4 @@ class NamespacePathValidator < AbstractPathValidator ...@@ -12,8 +12,4 @@ class NamespacePathValidator < AbstractPathValidator
def self.format_error_message def self.format_error_message
Gitlab::PathRegex.namespace_format_message Gitlab::PathRegex.namespace_format_message
end end
def self.full_path(record, value)
record.build_full_path
end
end end
...@@ -12,8 +12,4 @@ class ProjectPathValidator < AbstractPathValidator ...@@ -12,8 +12,4 @@ class ProjectPathValidator < AbstractPathValidator
def self.format_error_message def self.format_error_message
Gitlab::PathRegex.project_path_format_message Gitlab::PathRegex.project_path_format_message
end end
def self.full_path(record, value)
record.build_full_path
end
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 @@ ...@@ -57,4 +57,20 @@
.form-actions .form-actions
= button_to 'Remove group', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_group_message(@group) } = 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 = render 'shared/confirm_modal', phrase: @group.path
...@@ -88,7 +88,7 @@ ...@@ -88,7 +88,7 @@
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
#{ _('Members') } #{ _('Members') }
- if current_user && can?(current_user, :admin_group, @group) - 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 = link_to edit_group_path(@group) do
.nav-icon-container .nav-icon-container
= sprite_icon('settings') = sprite_icon('settings')
......
...@@ -184,10 +184,33 @@ ...@@ -184,10 +184,33 @@
Environments Environments
- if project_nav_tab? :clusters - if project_nav_tab? :clusters
- show_cluster_hint = show_gke_cluster_integration_callout?(@project)
= nav_link(controller: [:clusters, :user, :gcp]) do = 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 %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? - if @project.feature_available?(:builds, current_user) && !@project.empty_repo?
= nav_link(path: 'pipelines#charts') do = nav_link(path: 'pipelines#charts') do
......
...@@ -5,11 +5,11 @@ ...@@ -5,11 +5,11 @@
= s_('ClusterIntegration|Google Kubernetes Engine') = s_('ClusterIntegration|Google Kubernetes Engine')
%p %p
- link_gke = link_to(s_('ClusterIntegration|Google Kubernetes Engine'), @cluster.gke_cluster_url, target: '_blank', rel: 'noopener noreferrer') - 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 .well.form-group
%label.text-danger %label.text-danger
= s_('ClusterIntegration|Remove cluster integration') = s_('ClusterIntegration|Remove Kubernetes cluster integration')
%p %p
= s_("ClusterIntegration|Remove this cluster's configuration from this project. 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 cluster's integration? 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 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 .settings-content
.hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' } .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 %p.js-error-reason
.hidden.js-cluster-creating.alert.alert-info.alert-block.append-bottom-10{ role: 'alert' } .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' } .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 .gl-responsive-table-row
.table-section.section-30 .table-section.section-30
.table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Cluster") .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Kubernetes cluster")
.table-mobile-content .table-mobile-content
= link_to cluster.name, namespace_project_cluster_path(@project.namespace, @project, cluster) = link_to cluster.name, namespace_project_cluster_path(@project.namespace, @project, cluster)
.table-section.section-30 .table-section.section-30
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
.table-mobile-content .table-mobile-content
%button.js-project-feature-toggle.project-feature-toggle{ type: "button", %button.js-project-feature-toggle.project-feature-toggle{ type: "button",
class: "#{'is-checked' if cluster.enabled?} #{'is-disabled' if !cluster.can_toggle_cluster?}", 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?, disabled: !cluster.can_toggle_cluster?,
data: { endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } } data: { endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } }
%input.js-project-feature-toggle-input{ type: "hidden", value: cluster.enabled? } %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 .dropdown.clusters-dropdown
%button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', data: { toggle: 'dropdown' }, 'aria-haspopup': true, 'aria-expanded': false } %button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', data: { toggle: 'dropdown' }, 'aria-haspopup': true, 'aria-expanded': false }
...@@ -7,6 +7,6 @@ ...@@ -7,6 +7,6 @@
= icon('chevron-down') = icon('chevron-down')
%ul.dropdown-menu.clusters-dropdown-menu.dropdown-menu-full-width %ul.dropdown-menu.clusters-dropdown-menu.dropdown-menu-full-width
%li %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 %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 @@ ...@@ -3,10 +3,9 @@
.svg-content= image_tag 'illustrations/clusters_empty.svg' .svg-content= image_tag 'illustrations/clusters_empty.svg'
.col-xs-12 .col-xs-12
.text-content .text-content
%h4.text-center= s_('ClusterIntegration|Integrate cluster automation') %h4.text-center= s_('ClusterIntegration|Integrate Kubernetes 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') - 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|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} %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 .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 @@ ...@@ -5,15 +5,15 @@
%p %p
- if @cluster.enabled? - if @cluster.enabled?
- if can?(current_user, :update_cluster, @cluster) - 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 - else
= s_('ClusterIntegration|Cluster integration is enabled for this project.') = s_('ClusterIntegration|Kubernetes cluster integration is enabled for this project.')
- else - 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 %label.append-bottom-10.js-cluster-enable-toggle-area
%button{ type: 'button', %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)}", 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) } disabled: !can?(current_user, :update_cluster, @cluster) }
= field.hidden_field :enabled, { class: 'js-project-feature-toggle-input'} = field.hidden_field :enabled, { class: 'js-project-feature-toggle-input'}
%span.toggle-icon %span.toggle-icon
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
.form-group .form-group
%h5= s_('ClusterIntegration|Environment scope') %h5= s_('ClusterIntegration|Environment scope')
%p %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') = 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') = 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 %h4.prepend-top-0
= s_('ClusterIntegration|Cluster integration') = s_('ClusterIntegration|Kubernetes cluster integration')
%p %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 %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 } = s_('ClusterIntegration|Learn more about %{link_to_documentation}').html_safe % { link_to_documentation: link }
%p %p
- link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') - 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_for @cluster, html: { class: 'prepend-top-20' }, url: gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
= form_errors(@cluster) = form_errors(@cluster)
.form-group .form-group
= field.label :name, s_('ClusterIntegration|Cluster name') = field.label :name, s_('ClusterIntegration|Kubernetes cluster name')
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Cluster name') = field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Kubernetes cluster name')
.form-group .form-group
= field.label :environment_scope, s_('ClusterIntegration|Environment scope') = 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') = field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
...@@ -32,4 +32,4 @@ ...@@ -32,4 +32,4 @@
= provider_gcp_field.text_field :machine_type, class: 'form-control', placeholder: 'n1-standard-4' = provider_gcp_field.text_field :machine_type, class: 'form-control', placeholder: 'n1-standard-4'
.form-group .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 %h4.prepend-top-20
= s_('ClusterIntegration|Enter the details for your cluster') = s_('ClusterIntegration|Enter the details for your Kubernetes cluster')
%p %p
= s_('ClusterIntegration|Please make sure that your Google account meets the following requirements:') = s_('ClusterIntegration|Please make sure that your Google account meets the following requirements:')
%ul %ul
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
= s_('ClusterIntegration|Your account must have %{link_to_kubernetes_engine}').html_safe % { link_to_kubernetes_engine: link_to_kubernetes_engine } = s_('ClusterIntegration|Your account must have %{link_to_kubernetes_engine}').html_safe % { link_to_kubernetes_engine: link_to_kubernetes_engine }
%li %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') - 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 %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') - 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 .form-group
%label.append-bottom-10{ for: 'cluster-name' } %label.append-bottom-10{ for: 'cluster-name' }
= s_('ClusterIntegration|Cluster name') = s_('ClusterIntegration|Kubernetes cluster name')
.input-group .input-group
%input.form-control.cluster-name.js-select-on-focus{ value: @cluster.name, readonly: true } %input.form-control.cluster-name.js-select-on-focus{ value: @cluster.name, readonly: true }
%span.input-group-btn %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_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
= form_errors(@cluster) = form_errors(@cluster)
......
- breadcrumb_title "Cluster" - breadcrumb_title 'Kubernetes'
- page_title _("Login") - page_title _("Login")
.row.prepend-top-default .row.prepend-top-default
.col-sm-4 .col-sm-4
= render 'projects/clusters/sidebar' = render 'projects/clusters/sidebar'
.col-sm-8 .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' = render 'header'
.row .row
.col-sm-8.col-sm-offset-4.signin-with-google .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