File moved
...@@ -84,6 +84,9 @@ export default { ...@@ -84,6 +84,9 @@ export default {
ingressExternalIp() { ingressExternalIp() {
return this.applications.ingress.externalIp; return this.applications.ingress.externalIp;
}, },
certManagerInstalled() {
return this.applications.cert_manager.status === APPLICATION_STATUS.INSTALLED;
},
ingressDescription() { ingressDescription() {
const extraCostParagraph = sprintf( const extraCostParagraph = sprintf(
_.escape( _.escape(
...@@ -130,9 +133,9 @@ export default { ...@@ -130,9 +133,9 @@ export default {
return sprintf( return sprintf(
_.escape( _.escape(
s__( s__(
`ClusterIntegration|cert-manager is a native Kubernetes certificate management controller that helps with issuing certificates. `ClusterIntegration|Cert-Manager is a native Kubernetes certificate management controller that helps with issuing certificates.
Installing cert-manager on your cluster will issue a certificate by %{letsEncrypt} and ensure that certificates Installing Cert-Manager on your cluster will issue a certificate by %{letsEncrypt} and ensure that certificates
are valid and up to date.`, are valid and up-to-date.`,
), ),
), ),
{ {
...@@ -259,6 +262,16 @@ export default { ...@@ -259,6 +262,16 @@ export default {
</span> </span>
</div> </div>
<input v-else type="text" class="form-control js-ip-address" readonly value="?" /> <input v-else type="text" class="form-control js-ip-address" readonly value="?" />
<p class="form-text text-muted">
{{
s__(`ClusterIntegration|Point a wildcard DNS to this
generated IP address in order to access
your application after it has been deployed.`)
}}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
{{ __('More information') }}
</a>
</p>
</div> </div>
<p v-if="!ingressExternalIp" class="settings-message js-no-ip-message"> <p v-if="!ingressExternalIp" class="settings-message js-no-ip-message">
...@@ -272,17 +285,6 @@ export default { ...@@ -272,17 +285,6 @@ export default {
{{ __('More information') }} {{ __('More information') }}
</a> </a>
</p> </p>
<p>
{{
s__(`ClusterIntegration|Point a wildcard DNS to this
generated IP address in order to access
your application after it has been deployed.`)
}}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
{{ __('More information') }}
</a>
</p>
</template> </template>
<div v-html="ingressDescription"></div> <div v-html="ingressDescription"></div>
</div> </div>
...@@ -295,10 +297,41 @@ export default { ...@@ -295,10 +297,41 @@ export default {
:status-reason="applications.cert_manager.statusReason" :status-reason="applications.cert_manager.statusReason"
:request-status="applications.cert_manager.requestStatus" :request-status="applications.cert_manager.requestStatus"
:request-reason="applications.cert_manager.requestReason" :request-reason="applications.cert_manager.requestReason"
:install-application-request-params="{ email: applications.cert_manager.email }"
:disabled="!helmInstalled" :disabled="!helmInstalled"
title-link="https://cert-manager.readthedocs.io/en/latest/#" title-link="https://cert-manager.readthedocs.io/en/latest/#"
> >
<div slot="description" v-html="certManagerDescription"></div> <template>
<div slot="description">
<p v-html="certManagerDescription"></p>
<div class="form-group">
<label for="cert-manager-issuer-email">
{{ s__('ClusterIntegration|Issuer Email') }}
</label>
<div class="input-group">
<input
v-model="applications.cert_manager.email"
:readonly="certManagerInstalled"
type="text"
class="form-control js-email"
/>
</div>
<p class="form-text text-muted">
{{
s__(`ClusterIntegration|Issuers represent a certificate authority.
You must provide an email address for your Issuer. `)
}}
<a
href="http://docs.cert-manager.io/en/latest/reference/issuers.html?highlight=email"
target="_blank"
rel="noopener noreferrer"
>
{{ __('More information') }}
</a>
</p>
</div>
</div>
</template>
</application-row> </application-row>
<application-row <application-row
v-if="isProjectCluster" v-if="isProjectCluster"
...@@ -381,8 +414,8 @@ export default { ...@@ -381,8 +414,8 @@ export default {
/> />
</span> </span>
</div> </div>
</div>
<p v-if="ingressInstalled"> <p v-if="ingressInstalled" class="form-text text-muted">
{{ {{
s__(`ClusterIntegration|Replace this with your own hostname if you want. s__(`ClusterIntegration|Replace this with your own hostname if you want.
If you do so, point hostname to Ingress IP Address from above.`) If you do so, point hostname to Ingress IP Address from above.`)
...@@ -391,6 +424,7 @@ export default { ...@@ -391,6 +424,7 @@ export default {
{{ __('More information') }} {{ __('More information') }}
</a> </a>
</p> </p>
</div>
</template> </template>
</div> </div>
</application-row> </application-row>
... ...
......
...@@ -24,3 +24,4 @@ export const REQUEST_FAILURE = 'request-failure'; ...@@ -24,3 +24,4 @@ export const REQUEST_FAILURE = 'request-failure';
export const INGRESS = 'ingress'; export const INGRESS = 'ingress';
export const JUPYTER = 'jupyter'; export const JUPYTER = 'jupyter';
export const KNATIVE = 'knative'; export const KNATIVE = 'knative';
export const CERT_MANAGER = 'cert_manager';
import { s__ } from '../../locale'; import { s__ } from '../../locale';
import { INGRESS, JUPYTER, KNATIVE } from '../constants'; import { INGRESS, JUPYTER, KNATIVE, CERT_MANAGER } from '../constants';
export default class ClusterStore { export default class ClusterStore {
constructor() { constructor() {
...@@ -30,6 +30,7 @@ export default class ClusterStore { ...@@ -30,6 +30,7 @@ export default class ClusterStore {
statusReason: null, statusReason: null,
requestStatus: null, requestStatus: null,
requestReason: null, requestReason: null,
email: null,
}, },
runner: { runner: {
title: s__('ClusterIntegration|GitLab Runner'), title: s__('ClusterIntegration|GitLab Runner'),
...@@ -103,6 +104,9 @@ export default class ClusterStore { ...@@ -103,6 +104,9 @@ export default class ClusterStore {
if (appId === INGRESS) { if (appId === INGRESS) {
this.state.applications.ingress.externalIp = serverAppEntry.external_ip; this.state.applications.ingress.externalIp = serverAppEntry.external_ip;
} else if (appId === CERT_MANAGER) {
this.state.applications.cert_manager.email =
this.state.applications.cert_manager.email || serverAppEntry.email;
} else if (appId === JUPYTER) { } else if (appId === JUPYTER) {
this.state.applications.jupyter.hostname = this.state.applications.jupyter.hostname =
serverAppEntry.hostname || serverAppEntry.hostname ||
... ...
......
...@@ -192,8 +192,9 @@ export const toggleFileDiscussions = ({ getters, dispatch }, diff) => { ...@@ -192,8 +192,9 @@ export const toggleFileDiscussions = ({ getters, dispatch }, diff) => {
}); });
}; };
export const saveDiffDiscussion = ({ dispatch }, { note, formData }) => { export const saveDiffDiscussion = ({ state, dispatch }, { note, formData }) => {
const postData = getNoteFormData({ const postData = getNoteFormData({
commit: state.commit,
note, note,
...formData, ...formData,
}); });
... ...
......
...@@ -27,6 +27,7 @@ export const getReversePosition = linePosition => { ...@@ -27,6 +27,7 @@ export const getReversePosition = linePosition => {
export function getFormData(params) { export function getFormData(params) {
const { const {
commit,
note, note,
noteableType, noteableType,
noteableData, noteableData,
...@@ -66,7 +67,7 @@ export function getFormData(params) { ...@@ -66,7 +67,7 @@ export function getFormData(params) {
position, position,
noteable_type: noteableType, noteable_type: noteableType,
noteable_id: noteableData.id, noteable_id: noteableData.id,
commit_id: '', commit_id: commit && commit.id,
type: type:
diffFile.diff_refs.start_sha && diffFile.diff_refs.head_sha diffFile.diff_refs.start_sha && diffFile.diff_refs.head_sha
? DIFF_NOTE_TYPE ? DIFF_NOTE_TYPE
... ...
......
<script> <script>
import { __ } from '~/locale';
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
const HIDDEN_VALUE = '••••••';
export default { export default {
components: { components: {
GlButton, GlButton,
...@@ -13,17 +16,26 @@ export default { ...@@ -13,17 +16,26 @@ export default {
}, },
data() { data() {
return { return {
areVariablesVisible: false, showVariableValues: false,
}; };
}, },
computed: { computed: {
hasVariables() { hasVariables() {
return this.trigger.variables && this.trigger.variables.length > 0; return this.trigger.variables && this.trigger.variables.length > 0;
}, },
getToggleButtonText() {
return this.showVariableValues ? __('Hide values') : __('Reveal values');
},
hasValues() {
return this.trigger.variables.some(v => v.value);
},
}, },
methods: { methods: {
revealVariables() { toggleValues() {
this.areVariablesVisible = true; this.showVariableValues = !this.showVariableValues;
},
getDisplayValue(value) {
return this.showVariableValues ? value : HIDDEN_VALUE;
}, },
}, },
}; };
...@@ -33,31 +45,33 @@ export default { ...@@ -33,31 +45,33 @@ export default {
<div class="build-widget block"> <div class="build-widget block">
<h4 class="title">{{ __('Trigger') }}</h4> <h4 class="title">{{ __('Trigger') }}</h4>
<p v-if="trigger.short_token" class="js-short-token"> <p
v-if="trigger.short_token"
class="js-short-token"
:class="{ 'append-bottom-0': !hasVariables }"
>
<span class="build-light-text"> {{ __('Token') }} </span> {{ trigger.short_token }} <span class="build-light-text"> {{ __('Token') }} </span> {{ trigger.short_token }}
</p> </p>
<p v-if="hasVariables"> <template v-if="hasVariables">
<gl-button <p class="trigger-variables-btn-container">
v-if="!areVariablesVisible" <span class="build-light-text"> {{ __('Variables:') }} </span>
type="button"
class="btn btn-default group js-reveal-variables" <gl-button v-if="hasValues" class="group js-reveal-variables" @click="toggleValues">
@click="revealVariables" {{ getToggleButtonText }}
>
{{ __('Reveal Variables') }}
</gl-button> </gl-button>
</p> </p>
<dl v-if="areVariablesVisible" class="js-build-variables trigger-build-variables"> <table class="js-build-variables trigger-build-variables">
<template v-for="variable in trigger.variables"> <tr v-for="(variable, index) in trigger.variables" :key="`${variable.key}-${index}`">
<dt :key="`${variable.key}-variable`" class="js-build-variable trigger-build-variable"> <td class="js-build-variable trigger-build-variable trigger-variables-table-cell">
{{ variable.key }} {{ variable.key }}
</dt> </td>
<td class="js-build-value trigger-build-value trigger-variables-table-cell">
<dd :key="`${variable.key}-value`" class="js-build-value trigger-build-value"> {{ getDisplayValue(variable.value) }}
{{ variable.value }} </td>
</dd> </tr>
</table>
</template> </template>
</dl>
</div> </div>
</template> </template>
...@@ -7,3 +7,8 @@ export const addClassIfElementExists = (element, className) => { ...@@ -7,3 +7,8 @@ export const addClassIfElementExists = (element, className) => {
}; };
export const isInVueNoteablePage = () => isInIssuePage() || isInEpicPage() || isInMRPage(); export const isInVueNoteablePage = () => isInIssuePage() || isInEpicPage() || isInMRPage();
export const canScrollUp = ({ scrollTop }, margin = 0) => scrollTop > margin;
export const canScrollDown = ({ scrollTop, offsetHeight, scrollHeight }, margin = 0) =>
scrollTop + offsetHeight < scrollHeight - margin;
...@@ -155,7 +155,7 @@ export default class MilestoneSelect { ...@@ -155,7 +155,7 @@ export default class MilestoneSelect {
const { $el, e } = clickEvent; const { $el, e } = clickEvent;
let selected = clickEvent.selectedObj; let selected = clickEvent.selectedObj;
let data, boardsStore; let data, modalStoreFilter;
if (!selected) return; if (!selected) return;
if (options.handleClick) { if (options.handleClick) {
...@@ -179,11 +179,11 @@ export default class MilestoneSelect { ...@@ -179,11 +179,11 @@ export default class MilestoneSelect {
} }
if ($dropdown.closest('.add-issues-modal').length) { if ($dropdown.closest('.add-issues-modal').length) {
boardsStore = ModalStore.store.filter; modalStoreFilter = ModalStore.store.filter;
} }
if (boardsStore) { if (modalStoreFilter) {
boardsStore[$dropdown.data('fieldName')] = selected.name; modalStoreFilter[$dropdown.data('fieldName')] = selected.name;
e.preventDefault(); e.preventDefault();
} else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) { } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
return Issuable.filterResults($dropdown.closest('form')); return Issuable.filterResults($dropdown.closest('form'));
... ...
......
...@@ -247,15 +247,19 @@ Please check your network connection and try again.`; ...@@ -247,15 +247,19 @@ Please check your network connection and try again.`;
} else { } else {
this.reopenIssue() this.reopenIssue()
.then(() => this.enableButton()) .then(() => this.enableButton())
.catch(() => { .catch(({ data }) => {
this.enableButton(); this.enableButton();
this.toggleStateButtonLoading(false); this.toggleStateButtonLoading(false);
Flash( let errorMessage = sprintf(
sprintf(
__('Something went wrong while reopening the %{issuable}. Please try again later'), __('Something went wrong while reopening the %{issuable}. Please try again later'),
{ issuable: this.noteableDisplayName }, { issuable: this.noteableDisplayName },
),
); );
if (data) {
errorMessage = Object.values(data).join('\n');
}
Flash(errorMessage);
}); });
} }
}, },
... ...
......
...@@ -12,6 +12,10 @@ export default function notificationsDropdown() { ...@@ -12,6 +12,10 @@ export default function notificationsDropdown() {
const form = $(this).parents('.notification-form:first'); const form = $(this).parents('.notification-form:first');
form.find('.js-notification-loading').toggleClass('fa-bell fa-spin fa-spinner'); form.find('.js-notification-loading').toggleClass('fa-bell fa-spin fa-spinner');
if (form.hasClass('no-label')) {
form.find('.js-notification-loading').toggleClass('hidden');
form.find('.js-notifications-icon').toggleClass('hidden');
}
form.find('#notification_setting_level').val(notificationLevel); form.find('#notification_setting_level').val(notificationLevel);
form.submit(); form.submit();
}); });
... ...
......
...@@ -13,6 +13,9 @@ export default class Project { ...@@ -13,6 +13,9 @@ export default class Project {
const $cloneOptions = $('ul.clone-options-dropdown'); const $cloneOptions = $('ul.clone-options-dropdown');
const $projectCloneField = $('#project_clone'); const $projectCloneField = $('#project_clone');
const $cloneBtnLabel = $('.js-git-clone-holder .js-clone-dropdown-label'); const $cloneBtnLabel = $('.js-git-clone-holder .js-clone-dropdown-label');
const mobileCloneField = document.querySelector(
'.js-mobile-git-clone .js-clone-dropdown-label',
);
const selectedCloneOption = $cloneBtnLabel.text().trim(); const selectedCloneOption = $cloneBtnLabel.text().trim();
if (selectedCloneOption.length > 0) { if (selectedCloneOption.length > 0) {
...@@ -36,7 +39,11 @@ export default class Project { ...@@ -36,7 +39,11 @@ export default class Project {
$label.text(activeText); $label.text(activeText);
}); });
if (mobileCloneField) {
mobileCloneField.dataset.clipboardText = url;
} else {
$projectCloneField.val(url); $projectCloneField.val(url);
}
$('.js-git-empty .js-clone').text(url); $('.js-git-empty .js-clone').text(url);
}); });
// Ref switcher // Ref switcher
... ...
......
import Terminal from './terminal'; import Terminal from './terminal';
export default () => new Terminal({ selector: '#terminal' }); export default () => new Terminal(document.getElementById('terminal'));
import _ from 'underscore';
import $ from 'jquery'; import $ from 'jquery';
import { Terminal } from 'xterm'; import { Terminal } from 'xterm';
import * as fit from 'xterm/lib/addons/fit/fit'; import * as fit from 'xterm/lib/addons/fit/fit';
import { canScrollUp, canScrollDown } from '~/lib/utils/dom_utils';
const SCROLL_MARGIN = 5;
Terminal.applyAddon(fit);
export default class GLTerminal { export default class GLTerminal {
constructor(options = {}) { constructor(element, options = {}) {
this.options = Object.assign( this.options = Object.assign(
{}, {},
{ {
...@@ -13,7 +19,8 @@ export default class GLTerminal { ...@@ -13,7 +19,8 @@ export default class GLTerminal {
options, options,
); );
this.container = document.querySelector(options.selector); this.container = element;
this.onDispose = [];
this.setSocketUrl(); this.setSocketUrl();
this.createTerminal(); this.createTerminal();
...@@ -34,8 +41,6 @@ export default class GLTerminal { ...@@ -34,8 +41,6 @@ export default class GLTerminal {
} }
createTerminal() { createTerminal() {
Terminal.applyAddon(fit);
this.terminal = new Terminal(this.options); this.terminal = new Terminal(this.options);
this.socket = new WebSocket(this.socketUrl, ['terminal.gitlab.com']); this.socket = new WebSocket(this.socketUrl, ['terminal.gitlab.com']);
...@@ -72,4 +77,48 @@ export default class GLTerminal { ...@@ -72,4 +77,48 @@ export default class GLTerminal {
handleSocketFailure() { handleSocketFailure() {
this.terminal.write('\r\nConnection failure'); this.terminal.write('\r\nConnection failure');
} }
addScrollListener(onScrollLimit) {
const viewport = this.container.querySelector('.xterm-viewport');
const listener = _.throttle(() => {
onScrollLimit({
canScrollUp: canScrollUp(viewport, SCROLL_MARGIN),
canScrollDown: canScrollDown(viewport, SCROLL_MARGIN),
});
});
this.onDispose.push(() => viewport.removeEventListener('scroll', listener));
viewport.addEventListener('scroll', listener);
// don't forget to initialize value before scroll!
listener({ target: viewport });
}
disable() {
this.terminal.setOption('cursorBlink', false);
this.terminal.setOption('theme', { foreground: '#707070' });
this.terminal.setOption('disableStdin', true);
this.socket.close();
}
dispose() {
this.terminal.off('data');
this.terminal.dispose();
this.socket.close();
this.onDispose.forEach(fn => fn());
this.onDispose.length = 0;
}
scrollToTop() {
this.terminal.scrollToTop();
}
scrollToBottom() {
this.terminal.scrollToBottom();
}
fit() {
this.terminal.fit();
}
} }
...@@ -44,6 +44,7 @@ export default { ...@@ -44,6 +44,7 @@ export default {
class="sidebar-collapsed-icon" class="sidebar-collapsed-icon"
data-placement="left" data-placement="left"
data-container="body" data-container="body"
data-boundary="viewport"
@click="handleClick" @click="handleClick"
> >
<i aria-hidden="true" data-hidden="true" class="fa fa-tags"> </i> <i aria-hidden="true" data-hidden="true" class="fa fa-tags"> </i>
... ...
......
...@@ -21,6 +21,7 @@ ...@@ -21,6 +21,7 @@
&.s46 { @include avatar-size(46px, 15px); } &.s46 { @include avatar-size(46px, 15px); }
&.s48 { @include avatar-size(48px, 10px); } &.s48 { @include avatar-size(48px, 10px); }
&.s60 { @include avatar-size(60px, 12px); } &.s60 { @include avatar-size(60px, 12px); }
&.s64 { @include avatar-size(64px, 14px); }
&.s70 { @include avatar-size(70px, 14px); } &.s70 { @include avatar-size(70px, 14px); }
&.s90 { @include avatar-size(90px, 15px); } &.s90 { @include avatar-size(90px, 15px); }
&.s100 { @include avatar-size(100px, 15px); } &.s100 { @include avatar-size(100px, 15px); }
...@@ -80,6 +81,7 @@ ...@@ -80,6 +81,7 @@
&.s40 { font-size: 16px; line-height: 38px; } &.s40 { font-size: 16px; line-height: 38px; }
&.s48 { font-size: 20px; line-height: 46px; } &.s48 { font-size: 20px; line-height: 46px; }
&.s60 { font-size: 32px; line-height: 58px; } &.s60 { font-size: 32px; line-height: 58px; }
&.s64 { font-size: 32px; line-height: 64px; }
&.s70 { font-size: 34px; line-height: 70px; } &.s70 { font-size: 34px; line-height: 70px; }
&.s90 { font-size: 36px; line-height: 88px; } &.s90 { font-size: 36px; line-height: 88px; }
&.s100 { font-size: 36px; line-height: 98px; } &.s100 { font-size: 36px; line-height: 98px; }
... ...
......
...@@ -142,8 +142,14 @@ ...@@ -142,8 +142,14 @@
&.btn-sm { &.btn-sm {
padding: 4px 10px; padding: 4px 10px;
font-size: 13px; font-size: $gl-btn-small-font-size;
line-height: 18px; line-height: $gl-btn-small-line-height;
}
&.btn-xs {
padding: 2px $gl-btn-padding;
font-size: $gl-btn-small-font-size;
line-height: $gl-btn-small-line-height;
} }
&.btn-success, &.btn-success,
... ...
......
...@@ -386,3 +386,4 @@ img.emoji { ...@@ -386,3 +386,4 @@ img.emoji {
.flex-no-shrink { flex-shrink: 0; } .flex-no-shrink { flex-shrink: 0; }
.mw-460 { max-width: 460px; } .mw-460 { max-width: 460px; }
.ws-initial { white-space: initial; } .ws-initial { white-space: initial; }
.min-height-0 { min-height: 0; }
...@@ -383,6 +383,16 @@ ...@@ -383,6 +383,16 @@
top: 1px; top: 1px;
} }
} }
.dropdown-menu li a .identicon {
width: 17px;
height: 17px;
font-size: $gl-font-size-xs;
vertical-align: middle;
text-indent: 0;
line-height: $gl-font-size-xs + 2px;
display: inline-block;
}
} }
.breadcrumbs-list { .breadcrumbs-list {
... ...
......
...@@ -39,15 +39,6 @@ ...@@ -39,15 +39,6 @@
.git-clone-holder { .git-clone-holder {
display: none; display: none;
} }
// Display Star and Fork buttons without counters on mobile.
.project-repo-buttons {
display: block;
.count-buttons .count-badge {
margin-top: $gl-padding-8;
}
}
} }
.group-buttons { .group-buttons {
... ...
......