Skip to content
Snippets Groups Projects
Commit ea2cd431 authored by Kamil Trzcińśki's avatar Kamil Trzcińśki
Browse files

Single commit squash of all changes for...

Single commit squash of all changes for https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10878

It's needed due to https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/10777 being merged with squash.


Former-commit-id: aa440eb1
parent 4b0dce5a
No related branches found
No related tags found
No related merge requests found
Showing
with 775 additions and 167 deletions
import CANCELED_SVG from 'icons/_icon_status_canceled_borderless.svg';
import CREATED_SVG from 'icons/_icon_status_created_borderless.svg';
import FAILED_SVG from 'icons/_icon_status_failed_borderless.svg';
import MANUAL_SVG from 'icons/_icon_status_manual_borderless.svg';
import PENDING_SVG from 'icons/_icon_status_pending_borderless.svg';
import RUNNING_SVG from 'icons/_icon_status_running_borderless.svg';
import SKIPPED_SVG from 'icons/_icon_status_skipped_borderless.svg';
import SUCCESS_SVG from 'icons/_icon_status_success_borderless.svg';
import WARNING_SVG from 'icons/_icon_status_warning_borderless.svg';
const StatusIconEntityMap = {
icon_status_canceled: CANCELED_SVG,
icon_status_created: CREATED_SVG,
icon_status_failed: FAILED_SVG,
icon_status_manual: MANUAL_SVG,
icon_status_pending: PENDING_SVG,
icon_status_running: RUNNING_SVG,
icon_status_skipped: SKIPPED_SVG,
icon_status_success: SUCCESS_SVG,
icon_status_warning: WARNING_SVG,
};
export {
CANCELED_SVG,
CREATED_SVG,
FAILED_SVG,
MANUAL_SVG,
PENDING_SVG,
RUNNING_SVG,
SKIPPED_SVG,
SUCCESS_SVG,
WARNING_SVG,
StatusIconEntityMap as default,
};
Loading
Loading
@@ -49,6 +49,7 @@ import BlobForkSuggestion from './blob/blob_fork_suggestion';
import UserCallout from './user_callout';
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
import ShortcutsWiki from './shortcuts_wiki';
import Pipelines from './pipelines';
import BlobViewer from './blob/viewer/index';
import AutoWidthDropdownSelect from './issuable/auto_width_dropdown_select';
 
Loading
Loading
@@ -257,7 +258,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
const pipelineStatusUrl = `${document.querySelector('.js-pipeline-tab-link a').getAttribute('href')}/status.json`;
 
new gl.Pipelines({
new Pipelines({
initTabs: true,
pipelineStatusUrl,
tabsOptions: {
Loading
Loading
Loading
Loading
@@ -31,82 +31,78 @@
*
* ### How to use
*
* new window.gl.LinkedTabs({
* new LinkedTabs({
* action: "#{controller.action_name}",
* defaultAction: 'tab1',
* parentEl: '.tab-links'
* });
*/
 
(() => {
window.gl = window.gl || {};
export default class LinkedTabs {
/**
* Binds the events and activates de default tab.
*
* @param {Object} options
*/
constructor(options = {}) {
this.options = options;
 
window.gl.LinkedTabs = class LinkedTabs {
/**
* Binds the events and activates de default tab.
*
* @param {Object} options
*/
constructor(options) {
this.options = options || {};
this.defaultAction = this.options.defaultAction;
this.action = this.options.action || this.defaultAction;
 
this.defaultAction = this.options.defaultAction;
this.action = this.options.action || this.defaultAction;
if (this.action === 'show') {
this.action = this.defaultAction;
}
if (this.action === 'show') {
this.action = this.defaultAction;
}
 
this.currentLocation = window.location;
this.currentLocation = window.location;
 
const tabSelector = `${this.options.parentEl} a[data-toggle="tab"]`;
const tabSelector = `${this.options.parentEl} a[data-toggle="tab"]`;
 
// since this is a custom event we need jQuery :(
$(document)
.off('shown.bs.tab', tabSelector)
.on('shown.bs.tab', tabSelector, e => this.tabShown(e));
// since this is a custom event we need jQuery :(
$(document)
.off('shown.bs.tab', tabSelector)
.on('shown.bs.tab', tabSelector, e => this.tabShown(e));
 
this.activateTab(this.action);
}
this.activateTab(this.action);
}
 
/**
* Handles the `shown.bs.tab` event to set the currect url action.
*
* @param {type} evt
* @return {Function}
*/
tabShown(evt) {
const source = evt.target.getAttribute('href');
/**
* Handles the `shown.bs.tab` event to set the currect url action.
*
* @param {type} evt
* @return {Function}
*/
tabShown(evt) {
const source = evt.target.getAttribute('href');
 
return this.setCurrentAction(source);
}
return this.setCurrentAction(source);
}
 
/**
* Updates the URL with the path that matched the given action.
*
* @param {String} source
* @return {String}
*/
setCurrentAction(source) {
const copySource = source;
/**
* Updates the URL with the path that matched the given action.
*
* @param {String} source
* @return {String}
*/
setCurrentAction(source) {
const copySource = source;
 
copySource.replace(/\/+$/, '');
copySource.replace(/\/+$/, '');
 
const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`;
const newState = `${copySource}${this.currentLocation.search}${this.currentLocation.hash}`;
 
history.replaceState({
url: newState,
}, document.title, newState);
return newState;
}
history.replaceState({
url: newState,
}, document.title, newState);
return newState;
}
 
/**
* Given the current action activates the correct tab.
* http://getbootstrap.com/javascript/#tab-show
* Note: Will trigger `shown.bs.tab`
*/
activateTab() {
return $(`${this.options.parentEl} a[data-action='${this.action}']`).tab('show');
}
};
})();
/**
* Given the current action activates the correct tab.
* http://getbootstrap.com/javascript/#tab-show
* Note: Will trigger `shown.bs.tab`
*/
activateTab() {
return $(`${this.options.parentEl} a[data-action='${this.action}']`).tab('show');
}
}
/* eslint-disable no-new, guard-for-in, no-restricted-syntax, no-continue, no-param-reassign, max-len */
import LinkedTabs from './lib/utils/bootstrap_linked_tabs';
 
require('./lib/utils/bootstrap_linked_tabs');
((global) => {
class Pipelines {
constructor(options = {}) {
if (options.initTabs && options.tabsOptions) {
new global.LinkedTabs(options.tabsOptions);
}
if (options.pipelineStatusUrl) {
gl.utils.setCiStatusFavicon(options.pipelineStatusUrl);
}
this.addMarginToBuildColumns();
export default class Pipelines {
constructor(options = {}) {
if (options.initTabs && options.tabsOptions) {
// eslint-disable-next-line no-new
new LinkedTabs(options.tabsOptions);
}
 
addMarginToBuildColumns() {
this.pipelineGraph = document.querySelector('.js-pipeline-graph');
const secondChildBuildNodes = this.pipelineGraph.querySelectorAll('.build:nth-child(2)');
for (const buildNodeIndex in secondChildBuildNodes) {
const buildNode = secondChildBuildNodes[buildNodeIndex];
const firstChildBuildNode = buildNode.previousElementSibling;
if (!firstChildBuildNode || !firstChildBuildNode.matches('.build')) continue;
const multiBuildColumn = buildNode.closest('.stage-column');
const previousColumn = multiBuildColumn.previousElementSibling;
if (!previousColumn || !previousColumn.matches('.stage-column')) continue;
multiBuildColumn.classList.add('left-margin');
firstChildBuildNode.classList.add('left-connector');
const columnBuilds = previousColumn.querySelectorAll('.build');
if (columnBuilds.length === 1) previousColumn.classList.add('no-margin');
}
this.pipelineGraph.classList.remove('hidden');
if (options.pipelineStatusUrl) {
gl.utils.setCiStatusFavicon(options.pipelineStatusUrl);
}
}
global.Pipelines = Pipelines;
})(window.gl || (window.gl = {}));
}
<script>
import getActionIcon from '../../../vue_shared/ci_action_icons';
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
/**
* Renders either a cancel, retry or play icon pointing to the given path.
* TODO: Remove UJS from here and use an async request instead.
*/
export default {
props: {
tooltipText: {
type: String,
required: true,
},
link: {
type: String,
required: true,
},
actionMethod: {
type: String,
required: true,
},
actionIcon: {
type: String,
required: true,
},
},
mixins: [
tooltipMixin,
],
computed: {
actionIconSvg() {
return getActionIcon(this.actionIcon);
},
},
};
</script>
<template>
<a
:data-method="actionMethod"
:title="tooltipText"
:href="link"
ref="tooltip"
class="ci-action-icon-container"
data-toggle="tooltip"
data-container="body">
<i
class="ci-action-icon-wrapper"
v-html="actionIconSvg"
aria-hidden="true"
/>
</a>
</template>
<script>
import getActionIcon from '../../../vue_shared/ci_action_icons';
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
/**
* Renders either a cancel, retry or play icon pointing to the given path.
* TODO: Remove UJS from here and use an async request instead.
*/
export default {
props: {
tooltipText: {
type: String,
required: true,
},
link: {
type: String,
required: true,
},
actionMethod: {
type: String,
required: true,
},
actionIcon: {
type: String,
required: true,
},
},
mixins: [
tooltipMixin,
],
computed: {
actionIconSvg() {
return getActionIcon(this.actionIcon);
},
},
};
</script>
<template>
<a
:data-method="actionMethod"
:title="tooltipText"
:href="link"
ref="tooltip"
rel="nofollow"
class="ci-action-icon-wrapper js-ci-status-icon"
data-toggle="tooltip"
data-container="body"
v-html="actionIconSvg"
aria-label="Job's action">
</a>
</template>
<script>
import jobNameComponent from './job_name_component.vue';
import jobComponent from './job_component.vue';
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
/**
* Renders the dropdown for the pipeline graph.
*
* The following object should be provided as `job`:
*
* {
* "id": 4256,
* "name": "test",
* "status": {
* "icon": "icon_status_success",
* "text": "passed",
* "label": "passed",
* "group": "success",
* "details_path": "/root/ci-mock/builds/4256",
* "action": {
* "icon": "icon_action_retry",
* "title": "Retry",
* "path": "/root/ci-mock/builds/4256/retry",
* "method": "post"
* }
* }
* }
*/
export default {
props: {
job: {
type: Object,
required: true,
},
},
mixins: [
tooltipMixin,
],
components: {
jobComponent,
jobNameComponent,
},
computed: {
tooltipText() {
return `${this.job.name} - ${this.job.status.label}`;
},
},
};
</script>
<template>
<div>
<button
type="button"
data-toggle="dropdown"
data-container="body"
class="dropdown-menu-toggle build-content"
:title="tooltipText"
ref="tooltip">
<job-name-component
:name="job.name"
:status="job.status" />
<span class="dropdown-counter-badge">
{{job.size}}
</span>
</button>
<ul class="dropdown-menu big-pipeline-graph-dropdown-menu js-grouped-pipeline-dropdown">
<li class="scrollable-menu">
<ul>
<li v-for="item in job.jobs">
<job-component
:job="item"
:is-dropdown="true"
css-class-job-name="mini-pipeline-graph-dropdown-item"
/>
</li>
</ul>
</li>
</ul>
</div>
</template>
<script>
/* global Flash */
import Visibility from 'visibilityjs';
import Poll from '../../../lib/utils/poll';
import PipelineService from '../../services/pipeline_service';
import PipelineStore from '../../stores/pipeline_store';
import stageColumnComponent from './stage_column_component.vue';
import '../../../flash';
export default {
components: {
stageColumnComponent,
},
data() {
const DOMdata = document.getElementById('js-pipeline-graph-vue').dataset;
const store = new PipelineStore();
return {
isLoading: false,
endpoint: DOMdata.endpoint,
store,
state: store.state,
};
},
created() {
this.service = new PipelineService(this.endpoint);
const poll = new Poll({
resource: this.service,
method: 'getPipeline',
successCallback: this.successCallback,
errorCallback: this.errorCallback,
});
if (!Visibility.hidden()) {
this.isLoading = true;
poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
poll.restart();
} else {
poll.stop();
}
});
},
methods: {
successCallback(response) {
const data = response.json();
this.isLoading = false;
this.store.storeGraph(data.details.stages);
},
errorCallback() {
this.isLoading = false;
return new Flash('An error occurred while fetching the pipeline.');
},
capitalizeStageName(name) {
return name.charAt(0).toUpperCase() + name.slice(1);
},
},
};
</script>
<template>
<div class="build-content middle-block js-pipeline-graph">
<div class="pipeline-visualization pipeline-graph">
<div class="text-center">
<i
v-if="isLoading"
class="loading-icon fa fa-spin fa-spinner fa-3x"
aria-label="Loading"
aria-hidden="true" />
</div>
<ul
v-if="!isLoading"
class="stage-column-list">
<stage-column-component
v-for="stage in state.graph"
:title="capitalizeStageName(stage.name)"
:jobs="stage.groups"
:key="stage.name"/>
</ul>
</div>
</div>
</template>
<script>
import actionComponent from './action_component.vue';
import dropdownActionComponent from './dropdown_action_component.vue';
import jobNameComponent from './job_name_component.vue';
import tooltipMixin from '../../../vue_shared/mixins/tooltip';
/**
* Renders the badge for the pipeline graph and the job's dropdown.
*
* The following object should be provided as `job`:
*
* {
* "id": 4256,
* "name": "test",
* "status": {
* "icon": "icon_status_success",
* "text": "passed",
* "label": "passed",
* "group": "success",
* "details_path": "/root/ci-mock/builds/4256",
* "action": {
* "icon": "icon_action_retry",
* "title": "Retry",
* "path": "/root/ci-mock/builds/4256/retry",
* "method": "post"
* }
* }
* }
*/
export default {
props: {
job: {
type: Object,
required: true,
},
cssClassJobName: {
type: String,
required: false,
default: '',
},
isDropdown: {
type: Boolean,
required: false,
default: false,
},
},
components: {
actionComponent,
dropdownActionComponent,
jobNameComponent,
},
mixins: [
tooltipMixin,
],
computed: {
tooltipText() {
return `${this.job.name} - ${this.job.status.label}`;
},
/**
* Verifies if the provided job has an action path
*
* @return {Boolean}
*/
hasAction() {
return this.job.status && this.job.status.action && this.job.status.action.path;
},
},
};
</script>
<template>
<div>
<a
v-if="job.status.details_path"
:href="job.status.details_path"
:title="tooltipText"
:class="cssClassJobName"
ref="tooltip"
data-toggle="tooltip"
data-container="body">
<job-name-component
:name="job.name"
:status="job.status"
/>
</a>
<div
v-else
:title="tooltipText"
:class="cssClassJobName"
ref="tooltip"
data-toggle="tooltip"
data-container="body">
<job-name-component
:name="job.name"
:status="job.status"
/>
</div>
<action-component
v-if="hasAction && !isDropdown"
:tooltip-text="job.status.action.title"
:link="job.status.action.path"
:action-icon="job.status.action.icon"
:action-method="job.status.action.method"
/>
<dropdown-action-component
v-if="hasAction && isDropdown"
:tooltip-text="job.status.action.title"
:link="job.status.action.path"
:action-icon="job.status.action.icon"
:action-method="job.status.action.method"
/>
</div>
</template>
<script>
import ciIcon from '../../../vue_shared/components/ci_icon.vue';
/**
* Component that renders both the CI icon status and the job name.
* Used in
* - Badge component
* - Dropdown badge components
*/
export default {
props: {
name: {
type: String,
required: true,
},
status: {
type: Object,
required: true,
},
},
components: {
ciIcon,
},
};
</script>
<template>
<span>
<ci-icon
:status="status" />
<span class="ci-status-text">
{{name}}
</span>
</span>
</template>
<script>
import jobComponent from './job_component.vue';
import dropdownJobComponent from './dropdown_job_component.vue';
export default {
props: {
title: {
type: String,
required: true,
},
jobs: {
type: Array,
required: true,
},
},
components: {
jobComponent,
dropdownJobComponent,
},
methods: {
firstJob(list) {
return list[0];
},
jobId(job) {
return `ci-badge-${job.name}`;
},
},
};
</script>
<template>
<li class="stage-column">
<div class="stage-name">
{{title}}
</div>
<div class="builds-container">
<ul>
<li
v-for="job in jobs"
:key="job.id"
class="build"
:id="jobId(job)">
<div class="curve"></div>
<job-component
v-if="job.size === 1"
:job="job"
css-class-job-name="build-content"
/>
<dropdown-job-component
v-if="job.size > 1"
:job="job"
/>
</li>
</ul>
</div>
</li>
</template>
Loading
Loading
@@ -14,7 +14,7 @@
*/
 
/* global Flash */
import StatusIconEntityMap from '../../ci_status_icons';
import { statusCssClasses, borderlessStatusIconEntityMap } from '../../vue_shared/ci_status_icons';
 
export default {
props: {
Loading
Loading
@@ -109,11 +109,11 @@ export default {
},
 
triggerButtonClass() {
return `ci-status-icon-${this.stage.status.group}`;
return `ci-status-icon-${statusCssClasses[this.stage.status.icon]}`;
},
 
svgIcon() {
return StatusIconEntityMap[this.stage.status.icon];
return borderlessStatusIconEntityMap[this.stage.status.icon];
},
},
};
Loading
Loading
import Vue from 'vue';
import pipelineGraph from './components/graph/graph_component.vue';
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#js-pipeline-graph-vue',
components: {
pipelineGraph,
},
render: createElement => createElement('pipeline-graph'),
}));
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
export default class PipelineService {
constructor(endpoint) {
this.pipeline = Vue.resource(endpoint);
}
getPipeline() {
return this.pipeline.get();
}
}
export default class PipelineStore {
constructor() {
this.state = {};
this.state.graph = [];
}
storeGraph(graph = []) {
this.state.graph = graph;
}
}
import cancelSVG from 'icons/_icon_action_cancel.svg';
import retrySVG from 'icons/_icon_action_retry.svg';
import playSVG from 'icons/_icon_action_play.svg';
export default function getActionIcon(action) {
let icon;
switch (action) {
case 'icon_action_cancel':
icon = cancelSVG;
break;
case 'icon_action_retry':
icon = retrySVG;
break;
case 'icon_action_play':
icon = playSVG;
break;
default:
icon = '';
}
return icon;
}
import BORDERLESS_CANCELED_SVG from 'icons/_icon_status_canceled_borderless.svg';
import BORDERLESS_CREATED_SVG from 'icons/_icon_status_created_borderless.svg';
import BORDERLESS_FAILED_SVG from 'icons/_icon_status_failed_borderless.svg';
import BORDERLESS_MANUAL_SVG from 'icons/_icon_status_manual_borderless.svg';
import BORDERLESS_PENDING_SVG from 'icons/_icon_status_pending_borderless.svg';
import BORDERLESS_RUNNING_SVG from 'icons/_icon_status_running_borderless.svg';
import BORDERLESS_SKIPPED_SVG from 'icons/_icon_status_skipped_borderless.svg';
import BORDERLESS_SUCCESS_SVG from 'icons/_icon_status_success_borderless.svg';
import BORDERLESS_WARNING_SVG from 'icons/_icon_status_warning_borderless.svg';
import CANCELED_SVG from 'icons/_icon_status_canceled.svg';
import CREATED_SVG from 'icons/_icon_status_created.svg';
import FAILED_SVG from 'icons/_icon_status_failed.svg';
import MANUAL_SVG from 'icons/_icon_status_manual.svg';
import PENDING_SVG from 'icons/_icon_status_pending.svg';
import RUNNING_SVG from 'icons/_icon_status_running.svg';
import SKIPPED_SVG from 'icons/_icon_status_skipped.svg';
import SUCCESS_SVG from 'icons/_icon_status_success.svg';
import WARNING_SVG from 'icons/_icon_status_warning.svg';
export const borderlessStatusIconEntityMap = {
icon_status_canceled: BORDERLESS_CANCELED_SVG,
icon_status_created: BORDERLESS_CREATED_SVG,
icon_status_failed: BORDERLESS_FAILED_SVG,
icon_status_manual: BORDERLESS_MANUAL_SVG,
icon_status_pending: BORDERLESS_PENDING_SVG,
icon_status_running: BORDERLESS_RUNNING_SVG,
icon_status_skipped: BORDERLESS_SKIPPED_SVG,
icon_status_success: BORDERLESS_SUCCESS_SVG,
icon_status_warning: BORDERLESS_WARNING_SVG,
};
export const statusIconEntityMap = {
icon_status_canceled: CANCELED_SVG,
icon_status_created: CREATED_SVG,
icon_status_failed: FAILED_SVG,
icon_status_manual: MANUAL_SVG,
icon_status_pending: PENDING_SVG,
icon_status_running: RUNNING_SVG,
icon_status_skipped: SKIPPED_SVG,
icon_status_success: SUCCESS_SVG,
icon_status_warning: WARNING_SVG,
};
export const statusCssClasses = {
icon_status_canceled: 'canceled',
icon_status_created: 'created',
icon_status_failed: 'failed',
icon_status_manual: 'manual',
icon_status_pending: 'pending',
icon_status_running: 'running',
icon_status_skipped: 'skipped',
icon_status_success: 'success',
icon_status_warning: 'warning',
};
<script>
import { statusIconEntityMap, statusCssClasses } from '../../vue_shared/ci_status_icons';
export default {
props: {
status: {
type: Object,
required: true,
},
},
computed: {
statusIconSvg() {
return statusIconEntityMap[this.status.icon];
},
cssClass() {
const status = statusCssClasses[this.status.icon];
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
},
},
};
</script>
<template>
<span
:class="cssClass"
v-html="statusIconSvg">
</span>
</template>
export default {
mounted() {
$(this.$refs.tooltip).tooltip();
},
updated() {
$(this.$refs.tooltip).tooltip('fixTitle');
},
};
Loading
Loading
@@ -486,7 +486,7 @@
color: $gl-text-color-secondary;
 
// Action Icons in big pipeline-graph nodes
> .ci-action-icon-container .ci-action-icon-wrapper {
> div > .ci-action-icon-container .ci-action-icon-wrapper {
height: 30px;
width: 30px;
background: $white-light;
Loading
Loading
@@ -511,7 +511,7 @@
}
}
 
> .ci-action-icon-container {
> div > .ci-action-icon-container {
position: absolute;
right: 5px;
top: 5px;
Loading
Loading
@@ -541,7 +541,7 @@
}
}
 
> .build-content {
> div > .build-content {
display: inline-block;
padding: 8px 10px 9px;
width: 100%;
Loading
Loading
@@ -557,34 +557,6 @@
}
 
 
.arrow {
&::before,
&::after {
content: '';
display: inline-block;
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
top: 18px;
}
&::before {
left: -5px;
margin-top: -6px;
border-width: 7px 5px 7px 0;
border-right-color: $border-color;
}
&::after {
left: -4px;
margin-top: -9px;
border-width: 10px 7px 10px 0;
border-right-color: $white-light;
}
}
// Connect first build in each stage with right horizontal line
&:first-child {
&::after {
Loading
Loading
@@ -859,7 +831,8 @@
border-radius: 3px;
 
// build name
.ci-build-text {
.ci-build-text,
.ci-status-text {
font-weight: 200;
overflow: hidden;
white-space: nowrap;
Loading
Loading
@@ -911,6 +884,38 @@
}
}
 
/**
* Top arrow in the dropdown in the big pipeline graph
*/
.big-pipeline-graph-dropdown-menu {
&::before,
&::after {
content: '';
display: inline-block;
position: absolute;
width: 0;
height: 0;
border-color: transparent;
border-style: solid;
top: 18px;
}
&::before {
left: -5px;
margin-top: -6px;
border-width: 7px 5px 7px 0;
border-right-color: $border-color;
}
&::after {
left: -4px;
margin-top: -9px;
border-width: 10px 7px 10px 0;
border-right-color: $white-light;
}
}
/**
* Top arrow in the dropdown in the mini pipeline graph
*/
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment