Commit c814446b authored by Fernando Arias's avatar Fernando Arias

Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into move-job-cancel-btn

parents a536fe28 fe4f8cad
Pipeline #43039617 passed with stages
in 41 minutes and 2 seconds
......@@ -42,22 +42,35 @@ export function mergeUrlParams(params, url) {
return `${urlparts[1]}?${query}${urlparts[3]}`;
}
export function removeParamQueryString(url, param) {
const decodedUrl = decodeURIComponent(url);
const urlVariables = decodedUrl.split('&');
return urlVariables.filter(variable => variable.indexOf(param) === -1).join('&');
}
export function removeParams(params, source = window.location.href) {
const url = document.createElement('a');
url.href = source;
/**
* Removes specified query params from the url by returning a new url string that no longer
* includes the param/value pair. If no url is provided, `window.location.href` is used as
* the default value.
*
* @param {string[]} params - the query param names to remove
* @param {string} [url=windowLocation().href] - url from which the query param will be removed
* @returns {string} A copy of the original url but without the query param
*/
export function removeParams(params, url = window.location.href) {
const [rootAndQuery, fragment] = url.split('#');
const [root, query] = rootAndQuery.split('?');
if (!query) {
return url;
}
params.forEach(param => {
url.search = removeParamQueryString(url.search, param);
});
const encodedParams = params.map(param => encodeURIComponent(param));
const updatedQuery = query
.split('&')
.filter(paramPair => {
const [foundParam] = paramPair.split('=');
return encodedParams.indexOf(foundParam) < 0;
})
.join('&');
return url.href;
const writableQuery = updatedQuery.length > 0 ? `?${updatedQuery}` : '';
const writableFragment = fragment ? `#${fragment}` : '';
return `${root}${writableQuery}${writableFragment}`;
}
export function getLocationHash(url = window.location.href) {
......@@ -66,6 +79,20 @@ export function getLocationHash(url = window.location.href) {
return hashIndex === -1 ? null : url.substring(hashIndex + 1);
}
/**
* Apply the fragment to the given url by returning a new url string that includes
* the fragment. If the given url already contains a fragment, the original fragment
* will be removed.
*
* @param {string} url - url to which the fragment will be applied
* @param {string} fragment - fragment to append
*/
export const setUrlFragment = (url, fragment) => {
const [rootUrl] = url.split('#');
const encodedFragment = encodeURIComponent(fragment.replace(/^#/, ''));
return `${rootUrl}#${encodedFragment}`;
};
export function visitUrl(url, external = false) {
if (external) {
// Simulate `target="blank" rel="noopener noreferrer"`
......
<script>
import CodeCell from './code/index.vue';
import CodeOutput from './code/index.vue';
import OutputCell from './output/index.vue';
export default {
name: 'CodeCell',
components: {
'code-cell': CodeCell,
'output-cell': OutputCell,
CodeOutput,
OutputCell,
},
props: {
cell: {
......@@ -29,8 +30,8 @@ export default {
hasOutput() {
return this.cell.outputs.length;
},
output() {
return this.cell.outputs[0];
outputs() {
return this.cell.outputs;
},
},
};
......@@ -38,7 +39,7 @@ export default {
<template>
<div class="cell">
<code-cell
<code-output
:raw-code="rawInputCode"
:count="cell.execution_count"
:code-css-class="codeCssClass"
......@@ -47,7 +48,7 @@ export default {
<output-cell
v-if="hasOutput"
:count="cell.execution_count"
:output="output"
:outputs="outputs"
:code-css-class="codeCssClass"
/>
</div>
......
......@@ -3,8 +3,9 @@ import Prism from '../../lib/highlight';
import Prompt from '../prompt.vue';
export default {
name: 'CodeOutput',
components: {
prompt: Prompt,
Prompt,
},
props: {
count: {
......
......@@ -4,13 +4,21 @@ import Prompt from '../prompt.vue';
export default {
components: {
prompt: Prompt,
Prompt,
},
props: {
count: {
type: Number,
required: true,
},
rawCode: {
type: String,
required: true,
},
index: {
type: Number,
required: true,
},
},
computed: {
sanitizedOutput() {
......@@ -21,13 +29,16 @@ export default {
},
});
},
showOutput() {
return this.index === 0;
},
},
};
</script>
<template>
<div class="output">
<prompt />
<prompt type="Out" :count="count" :show-output="showOutput" />
<div v-html="sanitizedOutput"></div>
</div>
</template>
......@@ -6,6 +6,10 @@ export default {
prompt: Prompt,
},
props: {
count: {
type: Number,
required: true,
},
outputType: {
type: String,
required: true,
......@@ -14,10 +18,24 @@ export default {
type: String,
required: true,
},
index: {
type: Number,
required: true,
},
},
computed: {
imgSrc() {
return `data:${this.outputType};base64,${this.rawCode}`;
},
showOutput() {
return this.index === 0;
},
},
};
</script>
<template>
<div class="output"><prompt /> <img :src="'data:' + outputType + ';base64,' + rawCode" /></div>
<div class="output">
<prompt type="out" :count="count" :show-output="showOutput" /> <img :src="imgSrc" />
</div>
</template>
<script>
import CodeCell from '../code/index.vue';
import Html from './html.vue';
import Image from './image.vue';
import CodeOutput from '../code/index.vue';
import HtmlOutput from './html.vue';
import ImageOutput from './image.vue';
export default {
components: {
'code-cell': CodeCell,
'html-output': Html,
'image-output': Image,
},
props: {
codeCssClass: {
type: String,
......@@ -20,68 +15,69 @@ export default {
required: false,
default: 0,
},
output: {
type: Object,
outputs: {
type: Array,
required: true,
default: () => ({}),
},
},
computed: {
componentName() {
if (this.output.text) {
return 'code-cell';
} else if (this.output.data['image/png']) {
return 'image-output';
} else if (this.output.data['text/html']) {
return 'html-output';
} else if (this.output.data['image/svg+xml']) {
return 'html-output';
}
data() {
return {
outputType: '',
};
},
methods: {
dataForType(output, type) {
let data = output.data[type];
return 'code-cell';
},
rawCode() {
if (this.output.text) {
return this.output.text.join('');
if (typeof data === 'object') {
data = data.join('');
}
return this.dataForType(this.outputType);
return data;
},
outputType() {
if (this.output.text) {
return '';
} else if (this.output.data['image/png']) {
return 'image/png';
} else if (this.output.data['text/html']) {
return 'text/html';
} else if (this.output.data['image/svg+xml']) {
return 'image/svg+xml';
getComponent(output) {
if (output.text) {
return CodeOutput;
} else if (output.data['image/png']) {
this.outputType = 'image/png';
return ImageOutput;
} else if (output.data['text/html']) {
this.outputType = 'text/html';
return HtmlOutput;
} else if (output.data['image/svg+xml']) {
this.outputType = 'image/svg+xml';
return HtmlOutput;
}
return 'text/plain';
this.outputType = 'text/plain';
return CodeOutput;
},
},
methods: {
dataForType(type) {
let data = this.output.data[type];
if (typeof data === 'object') {
data = data.join('');
rawCode(output) {
if (output.text) {
return output.text.join('');
}
return data;
return this.dataForType(output, this.outputType);
},
},
};
</script>
<template>
<component
:is="componentName"
:output-type="outputType"
:count="count"
:raw-code="rawCode"
:code-css-class="codeCssClass"
type="output"
/>
<div>
<component
:is="getComponent(output)"
v-for="(output, index) in outputs"
:key="index"
type="output"
:output-type="outputType"
:count="count"
:index="index"
:raw-code="rawCode(output)"
:code-css-class="codeCssClass"
/>
</div>
</template>
......@@ -11,18 +11,26 @@ export default {
required: false,
default: 0,
},
showOutput: {
type: Boolean,
required: false,
default: true,
},
},
computed: {
hasKeys() {
return this.type !== '' && this.count;
},
showTypeText() {
return this.type && this.count && this.showOutput;
},
},
};
</script>
<template>
<div class="prompt">
<span v-if="hasKeys"> {{ type }} [{{ count }}]: </span>
<span v-if="showTypeText"> {{ type }} [{{ count }}]: </span>
</div>
</template>
......
......@@ -3,8 +3,8 @@ import { MarkdownCell, CodeCell } from './cells';
export default {
components: {
'code-cell': CodeCell,
'markdown-cell': MarkdownCell,
CodeCell,
MarkdownCell,
},
props: {
notebook: {
......
......@@ -370,7 +370,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
>
<button
:disabled="isSubmitButtonDisabled"
class="btn btn-create comment-btn js-comment-button js-comment-submit-button
class="btn btn-success js-comment-button js-comment-submit-button
qa-comment-button"
type="submit"
@click.prevent="handleSave();"
......@@ -381,7 +381,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
:disabled="isSubmitButtonDisabled"
name="button"
type="button"
class="btn comment-btn note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown"
class="btn btn-success note-type-toggle js-note-new-discussion dropdown-toggle qa-note-dropdown"
data-display="static"
data-toggle="dropdown"
aria-label="Open comment type dropdown"
......
<script>
import $ from 'jquery';
import { mapGetters, mapActions } from 'vuex';
import { getLocationHash } from '../../lib/utils/url_utility';
import Icon from '~/vue_shared/components/icon.vue';
import {
DISCUSSION_FILTERS_DEFAULT_VALUE,
......@@ -44,29 +45,47 @@ export default {
eventHub.$on('MergeRequestTabChange', this.toggleFilters);
this.toggleFilters(currentTab);
}
window.addEventListener('hashchange', this.handleLocationHash);
this.handleLocationHash();
},
mounted() {
this.toggleCommentsForm();
},
destroyed() {
window.removeEventListener('hashchange', this.handleLocationHash);
},
methods: {
...mapActions(['filterDiscussion', 'setCommentsDisabled']),
...mapActions(['filterDiscussion', 'setCommentsDisabled', 'setTargetNoteHash']),
selectFilter(value) {
const filter = parseInt(value, 10);
// close dropdown
$(this.$refs.dropdownToggle).dropdown('toggle');
this.toggleDropdown();
if (filter === this.currentValue) return;
this.currentValue = filter;
this.filterDiscussion({ path: this.getNotesDataByProp('discussionsPath'), filter });
this.toggleCommentsForm();
},
toggleDropdown() {
$(this.$refs.dropdownToggle).dropdown('toggle');
},
toggleCommentsForm() {
this.setCommentsDisabled(this.currentValue === HISTORY_ONLY_FILTER_VALUE);
},
toggleFilters(tab) {
this.displayFilters = tab === DISCUSSION_TAB_LABEL;
},
handleLocationHash() {
const hash = getLocationHash();
if (/^note_/.test(hash) && this.currentValue !== DISCUSSION_FILTERS_DEFAULT_VALUE) {
this.selectFilter(this.defaultValue);
this.toggleDropdown(); // close dropdown
this.setTargetNoteHash(hash);
}
},
},
};
</script>
......
......@@ -2,6 +2,7 @@ import $ from 'jquery';
import UsernameValidator from './username_validator';
import SigninTabsMemoizer from './signin_tabs_memoizer';
import OAuthRememberMe from './oauth_remember_me';
import preserveUrlFragment from './preserve_url_fragment';
document.addEventListener('DOMContentLoaded', () => {
new UsernameValidator(); // eslint-disable-line no-new
......@@ -10,4 +11,8 @@ document.addEventListener('DOMContentLoaded', () => {
new OAuthRememberMe({
container: $('.omniauth-container'),
}).bindEvents();
// Save the URL fragment from the current window location. This will be present if the user was
// redirected to sign-in after attempting to access a protected URL that included a fragment.
preserveUrlFragment(window.location.hash);
});
import $ from 'jquery';
import { mergeUrlParams, removeParams } from '~/lib/utils/url_utility';
/**
* OAuth-based login buttons have a separate "remember me" checkbox.
......@@ -24,9 +25,9 @@ export default class OAuthRememberMe {
const href = $(element).attr('href');
if (rememberMe) {
$(element).attr('href', `${href}?remember_me=1`);
$(element).attr('href', mergeUrlParams({ remember_me: 1 }, href));
} else {
$(element).attr('href', href.replace('?remember_me=1', ''));
$(element).attr('href', removeParams(['remember_me'], href));
}
});
}
......
import { mergeUrlParams, setUrlFragment } from '~/lib/utils/url_utility';
/**
* Ensure the given URL fragment is preserved by appending it to sign-in/sign-up form actions and
* OAuth/SAML login links.
*
* @param fragment {string} - url fragment to be preserved
*/
export default function preserveUrlFragment(fragment = '') {
if (fragment) {
const normalFragment = fragment.replace(/^#/, '');
// Append the fragment to all sign-in/sign-up form actions so it is preserved when the user is
// eventually redirected back to the originally requested URL.
const forms = document.querySelectorAll('#signin-container form');
Array.prototype.forEach.call(forms, form => {
const actionWithFragment = setUrlFragment(form.getAttribute('action'), `#${normalFragment}`);
form.setAttribute('action', actionWithFragment);
});
// Append a redirect_fragment query param to all oauth provider links. The redirect_fragment
// query param will be available in the omniauth callback upon successful authentication
const anchors = document.querySelectorAll('#signin-container a.oauth-login');
Array.prototype.forEach.call(anchors, anchor => {
const newHref = mergeUrlParams(
{ redirect_fragment: normalFragment },
anchor.getAttribute('href'),
);
anchor.setAttribute('href', newHref);
});
}
}
<script>
import PodBox from './pod_box.vue';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
Icon,
PodBox,
ClipboardButton,
},
props: {
func: {
type: Object,
required: true,
},
},
computed: {
name() {
return this.func.name;
},
description() {
return this.func.description;
},
funcUrl() {
return this.func.url;
},
podCount() {
return this.func.podcount || 0;
},
},
};
</script>
<template>
<section id="serverless-function-details">
<h3>{{ name }}</h3>
<div class="append-bottom-default">
<div v-for="line in description.split('\n')" :key="line">{{ line }}<br /></div>
</div>
<div class="clipboard-group append-bottom-default">
<div class="label label-monospace">{{ funcUrl }}</div>
<clipboard-button
:text="String(funcUrl)"
:title="s__('ServerlessDetails|Copy URL to clipboard')"
class="input-group-text js-clipboard-btn"
/>
<a
:href="funcUrl"
target="_blank"
rel="noopener noreferrer nofollow"
class="input-group-text btn btn-default"
>
<icon name="external-link" />
</a>
</div>
<h4>{{ s__('ServerlessDetails|Kubernetes Pods') }}</h4>
<div v-if="podCount > 0">
<p>
<b v-if="podCount == 1">{{ podCount }} {{ s__('ServerlessDetails|pod in use') }}</b>
<b v-else>{{ podCount }} {{ s__('ServerlessDetails|pods in use') }}</b>
</p>
<pod-box :count="podCount" />
<p>
{{
s__('ServerlessDetails|Number of Kubernetes pods in use over time based on necessity.')
}}
</p>
</div>
<div v-else><p>No pods loaded at this time.</p></div>
</section>
</template>
......@@ -15,8 +15,14 @@ export default {
name() {
return this.func.name;
},
url() {
return this.func.url;
description() {
return this.func.description;
},
detailUrl() {
return this.func.detail_url;
},
environment() {
return this.func.environment_scope;
},
image() {
return this.func.image;
......@@ -30,11 +36,20 @@ export default {
<template>
<div class="gl-responsive-table-row">
<div class="table-section section-20">{{ name }}</div>
<div class="table-section section-50">
<a :href="url">{{ url }}</a>
<div class="table-section section-20 section-wrap">
<a :href="detailUrl">{{ name }}</a>
</div>
<div class="table-section section-10">{{ environment }}</div>
<div class="table-section section-40 section-wrap">
<span class="line-break">{{ description }}</span>
</div>
<div class="table-section section-20">{{ image }}</div>
<div class="table-section section-10"><timeago :time="timestamp" /></div>
</div>
</template>
<style>
.line-break {
white-space: pre;
}
</style>
......@@ -50,8 +50,11 @@ export default {
<div class="table-section section-20" role="rowheader">
{{ s__('Serverless|Function') }}
</div>
<div class="table-section section-50" role="rowheader">
{{ s__('Serverless|Domain') }}
<div class="table-section section-10" role="rowheader">
{{ s__('Serverless|Cluster Env') }}
</div>
<div class="table-section section-40" role="rowheader">
{{ s__('Serverless|Description') }}
</div>
<div class="table-section section-20" role="rowheader">
{{ s__('Serverless|Runtime') }}
......