Commit 6d7a5248 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into feature/multi-level-container-registry-images

* master: (94 commits)
  Merge branch 'open-redirect-fix-continue-to' into 'security'
  Merge branch 'open-redirect-host-fix' into 'security'
  Merge branch 'path-disclosure-proj-import-export' into 'security'
  Merge branch '29364-private-projects-mr-fix'
  Merge branch '30125-markdown-security'
  Issue title realtime
  Update CHANGELOG.md for 8.16.9
  Update CHANGELOG.md for 8.17.5
  Update CHANGELOG.md for 9.0.4
  Add "search" optional param and docs for V4
  Use PDFLab to render PDFs in GitLab
  Separate Scala from Java in CI examples
  Fix broken link
  Reorganize CI examples, add more links
  Refactor CI index page
  Remove deprecated field from workhorse response
  Use gitlab-workhorse 1.4.3
  Document how ETag caching middleware handles query parameters
  Make group skip validation in the frontend
  Use NamespaceValidator::WILDCARD_ROUTES in ETag caching middleware
  ...
parents 714c408f aaa49c2c
...@@ -8,6 +8,7 @@ cache: ...@@ -8,6 +8,7 @@ cache:
variables: variables:
MYSQL_ALLOW_EMPTY_PASSWORD: "1" MYSQL_ALLOW_EMPTY_PASSWORD: "1"
RAILS_ENV: "test" RAILS_ENV: "test"
NODE_ENV: "test"
SIMPLECOV: "true" SIMPLECOV: "true"
SETUP_DB: "true" SETUP_DB: "true"
USE_BUNDLE_INSTALL: "true" USE_BUNDLE_INSTALL: "true"
...@@ -129,9 +130,7 @@ setup-test-env: ...@@ -129,9 +130,7 @@ setup-test-env:
stage: prepare stage: prepare
script: script:
- node --version - node --version
- yarn --version
- yarn install --pure-lockfile - yarn install --pure-lockfile
- yarn check # ensure that yarn.lock matches package.json
- bundle exec rake gitlab:assets:compile - bundle exec rake gitlab:assets:compile
- bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init' - bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
artifacts: artifacts:
...@@ -296,8 +295,6 @@ docs:check:apilint: ...@@ -296,8 +295,6 @@ docs:check:apilint:
image: "phusion/baseimage" image: "phusion/baseimage"
stage: test stage: test
<<: *dedicated-runner <<: *dedicated-runner
variables:
GIT_DEPTH: "3"
cache: {} cache: {}
dependencies: [] dependencies: []
before_script: [] before_script: []
...@@ -308,8 +305,6 @@ docs:check:links: ...@@ -308,8 +305,6 @@ docs:check:links:
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine" image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
stage: test stage: test
<<: *dedicated-runner <<: *dedicated-runner
variables:
GIT_DEPTH: "3"
cache: {} cache: {}
dependencies: [] dependencies: []
before_script: [] before_script: []
......
...@@ -2,6 +2,31 @@ ...@@ -2,6 +2,31 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 9.0.4 (2017-04-05)
- Don’t show source project name when user does not have access.
- Remove the class attribute from the whitelist for HTML generated from Markdown.
- Fix path disclosure in project import/export.
- Fix for open redirect vulnerability using continue[to] in URL when requesting project import status.
- Fix for open redirect vulnerabilities in todos, issues, and MR controllers.
## 9.0.3 (2017-04-05)
- Fix name colision when importing GitHub pull requests from forked repositories. !9719
- Fix GitHub Importer for PRs of deleted forked repositories. !9992
- Fix environment folder route when special chars present in environment name. !10250
- Improve Markdown rendering when a lot of merge requests are referenced. !10252
- Allow users to import GitHub projects to subgroups.
- Backport API changes needed to fix sticking in EE.
- Remove unnecessary ORDER BY clause from `forked_to_project_id` subquery. (mhasbini)
- Make CI build to use optimistic locking only on status change.
- Fix race condition where a namespace would be deleted before a project was deleted.
- Fix linking to new issue with selected template via url parameter.
- Remove unnecessary ORDER BY clause when updating todos. (mhasbini)
- API: Make the /notes endpoint work with noteable iid instead of id.
- Fixes method not replacing URL parameters correctly and breaking pipelines pagination.
- Move issue, mr, todos next to profile dropdown in top nav.
## 9.0.2 (2017-03-29) ## 9.0.2 (2017-03-29)
- Correctly update paths when changing a child group. - Correctly update paths when changing a child group.
...@@ -303,6 +328,14 @@ entry. ...@@ -303,6 +328,14 @@ entry.
- Change development tanuki favicon colors to match logo color order. - Change development tanuki favicon colors to match logo color order.
- API issues - support filtering by iids. - API issues - support filtering by iids.
## 8.17.5 (2017-04-05)
- Don’t show source project name when user does not have access.
- Remove the class attribute from the whitelist for HTML generated from Markdown.
- Fix path disclosure in project import/export.
- Fix for open redirect vulnerability using continue[to] in URL when requesting project import status.
- Fix for open redirect vulnerabilities in todos, issues, and MR controllers.
## 8.17.4 (2017-03-19) ## 8.17.4 (2017-03-19)
- Only show public emails in atom feeds. - Only show public emails in atom feeds.
...@@ -516,6 +549,14 @@ entry. ...@@ -516,6 +549,14 @@ entry.
- Remove deprecated GitlabCiService. - Remove deprecated GitlabCiService.
- Requeue pending deletion projects. - Requeue pending deletion projects.
## 8.16.9 (2017-04-05)
- Don’t show source project name when user does not have access.
- Remove the class attribute from the whitelist for HTML generated from Markdown.
- Fix path disclosure in project import/export.
- Fix for open redirect vulnerability using continue[to] in URL when requesting project import status.
- Fix for open redirect vulnerabilities in todos, issues, and MR controllers.
## 8.16.8 (2017-03-19) ## 8.16.8 (2017-03-19)
- Only show public emails in atom feeds. - Only show public emails in atom feeds.
......
...@@ -223,7 +223,7 @@ gem 'oj', '~> 2.17.4' ...@@ -223,7 +223,7 @@ gem 'oj', '~> 2.17.4'
gem 'chronic', '~> 0.10.2' gem 'chronic', '~> 0.10.2'
gem 'chronic_duration', '~> 0.10.6' gem 'chronic_duration', '~> 0.10.6'
gem 'webpack-rails', '~> 0.9.9' gem 'webpack-rails', '~> 0.9.10'
gem 'rack-proxy', '~> 0.6.0' gem 'rack-proxy', '~> 0.6.0'
gem 'sass-rails', '~> 5.0.6' gem 'sass-rails', '~> 5.0.6'
......
...@@ -823,8 +823,8 @@ GEM ...@@ -823,8 +823,8 @@ GEM
addressable (>= 2.3.6) addressable (>= 2.3.6)
crack (>= 0.3.2) crack (>= 0.3.2)
hashdiff hashdiff
webpack-rails (0.9.9) webpack-rails (0.9.10)
rails (>= 3.2.0) railties (>= 3.2.0)
websocket-driver (0.6.3) websocket-driver (0.6.3)
websocket-extensions (>= 0.1.0) websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.2) websocket-extensions (0.1.2)
...@@ -1026,7 +1026,7 @@ DEPENDENCIES ...@@ -1026,7 +1026,7 @@ DEPENDENCIES
virtus (~> 1.0.1) virtus (~> 1.0.1)
vmstat (~> 2.3.0) vmstat (~> 2.3.0)
webmock (~> 1.24.0) webmock (~> 1.24.0)
webpack-rails (~> 0.9.9) webpack-rails (~> 0.9.10)
wikicloth (= 0.8.1) wikicloth (= 0.8.1)
BUNDLED WITH BUNDLED WITH
......
...@@ -476,10 +476,10 @@ AwardsHandler.prototype.setupSearch = function setupSearch() { ...@@ -476,10 +476,10 @@ AwardsHandler.prototype.setupSearch = function setupSearch() {
this.registerEventListener('on', $('input.emoji-search'), 'input', (e) => { this.registerEventListener('on', $('input.emoji-search'), 'input', (e) => {
const term = $(e.target).val().trim(); const term = $(e.target).val().trim();
// Clean previous search results // Clean previous search results
$('ul.emoji-menu-search, h5.emoji-search').remove(); $('ul.emoji-menu-search, h5.emoji-search-title').remove();
if (term.length > 0) { if (term.length > 0) {
// Generate a search result block // Generate a search result block
const h5 = $('<h5 class="emoji-search" />').text('Search results'); const h5 = $('<h5 class="emoji-search-title"/>').text('Search results');
const foundEmojis = this.searchEmojis(term).show(); const foundEmojis = this.searchEmojis(term).show();
const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis); const ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis);
$('.emoji-menu-content ul, .emoji-menu-content h5').hide(); $('.emoji-menu-content ul, .emoji-menu-content h5').hide();
......
/* eslint-disable no-new */
import Vue from 'vue';
import PDFLab from 'vendor/pdflab';
import workerSrc from 'vendor/pdf.worker';
Vue.use(PDFLab, {
workerSrc,
});
export default () => {
const el = document.getElementById('js-pdf-viewer');
new Vue({
el,
data() {
return {
error: false,
loadError: false,
loading: true,
pdf: el.dataset.endpoint,
};
},
methods: {
onLoad() {
this.loading = false;
},
onError(error) {
this.loading = false;
this.loadError = true;
this.error = error;
},
},
template: `
<div class="container-fluid md prepend-top-default append-bottom-default">
<div
class="text-center loading"
v-if="loading && !error">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true"
aria-label="PDF loading">
</i>
</div>
<pdf-lab
v-if="!loadError"
:pdf="pdf"
@pdflabload="onLoad"
@pdflaberror="onError" />
<p
class="text-center"
v-if="error">
<span v-if="loadError">
An error occured whilst loading the file. Please try again later.
</span>
<span v-else>
An error occured whilst decoding the file.
</span>
</p>
</div>
`,
});
};
import renderPDF from './pdf';
document.addEventListener('DOMContentLoaded', renderPDF);
import JSZip from 'jszip';
import JSZipUtils from 'jszip-utils';
export default class SketchLoader {
constructor(container) {
this.container = container;
this.loadingIcon = this.container.querySelector('.js-loading-icon');
this.load();
}
load() {
return this.getZipFile()
.then(data => JSZip.loadAsync(data))
.then(asyncResult => asyncResult.files['previews/preview.png'].async('uint8array'))
.then((content) => {
const url = window.URL || window.webkitURL;
const blob = new Blob([new Uint8Array(content)], {
type: 'image/png',
});
const previewUrl = url.createObjectURL(blob);
this.render(previewUrl);
})
.catch(this.error.bind(this));
}
getZipFile() {
return new JSZip.external.Promise((resolve, reject) => {
JSZipUtils.getBinaryContent(this.container.dataset.endpoint, (err, data) => {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
render(previewUrl) {
const previewLink = document.createElement('a');
const previewImage = document.createElement('img');
previewLink.href = previewUrl;
previewLink.target = '_blank';
previewImage.src = previewUrl;
previewImage.className = 'img-responsive';
previewLink.appendChild(previewImage);
this.container.appendChild(previewLink);
this.removeLoadingIcon();
}
error() {
const errorMsg = document.createElement('p');
errorMsg.className = 'prepend-top-default append-bottom-default text-center';
errorMsg.textContent = `
Cannot show preview. For previews on sketch files, they must have the file format
introduced by Sketch version 43 and above.
`;
this.container.appendChild(errorMsg);
this.removeLoadingIcon();
}
removeLoadingIcon() {
if (this.loadingIcon) {
this.loadingIcon.remove();
}
}
}
/* eslint-disable no-new */
import SketchLoader from './sketch';
document.addEventListener('DOMContentLoaded', () => {
const el = document.getElementById('js-sketch-viewer');
new SketchLoader(el);
});
/* eslint-disable comma-dangle, space-before-function-paren, one-var */ /* eslint-disable comma-dangle, space-before-function-paren, one-var */
/* global Sortable */ /* global Sortable */
import Vue from 'vue'; import Vue from 'vue';
import boardList from './board_list';
import boardBlankState from './board_blank_state'; import boardBlankState from './board_blank_state';
require('./board_delete'); require('./board_delete');
...@@ -16,7 +16,7 @@ require('./board_list'); ...@@ -16,7 +16,7 @@ require('./board_list');
gl.issueBoards.Board = Vue.extend({ gl.issueBoards.Board = Vue.extend({
template: '#js-board-template', template: '#js-board-template',
components: { components: {
'board-list': gl.issueBoards.BoardList, boardList,
'board-delete': gl.issueBoards.BoardDelete, 'board-delete': gl.issueBoards.BoardDelete,
boardBlankState, boardBlankState,
}, },
......
/* eslint-disable comma-dangle, space-before-function-paren, max-len */
/* global Sortable */ /* global Sortable */
import Vue from 'vue';
import boardNewIssue from './board_new_issue'; import boardNewIssue from './board_new_issue';
import boardCard from './board_card'; import boardCard from './board_card';
import eventHub from '../eventhub';
(() => { const Store = gl.issueBoards.BoardsStore;
const Store = gl.issueBoards.BoardsStore;
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardList = Vue.extend({ export default {
template: '#js-board-list-template', name: 'BoardList',
components: {
boardCard,
boardNewIssue,
},
props: { props: {
disabled: Boolean, disabled: {
list: Object, type: Boolean,
issues: Array, required: true,
loading: Boolean, },
issueLinkBase: String, list: {
rootPath: String, type: Object,
}, required: true,
data () { },
issues: {
type: Array,
required: true,
},
loading: {
type: Boolean,
required: true,
},
issueLinkBase: {
type: String,
required: true,
},
rootPath: {
type: String,
required: true,
},
},
data() {
return { return {
scrollOffset: 250, scrollOffset: 250,
filters: Store.state.filters, filters: Store.state.filters,
showCount: false, showCount: false,
showIssueForm: false showIssueForm: false,
}; };
}, },
watch: { components: {
filters: { boardCard,
handler () { boardNewIssue,
this.list.loadingMore = false;
this.$refs.list.scrollTop = 0;
},
deep: true
},
issues () {
this.$nextTick(() => {
if (this.scrollHeight() <= this.listHeight() && this.list.issuesSize > this.list.issues.length) {
this.list.page += 1;
this.list.getIssues(false);
}
if (this.scrollHeight() > Math.ceil(this.listHeight())) {
this.showCount = true;
} else {
this.showCount = false;
}
});
}
}, },
methods: { methods: {
listHeight () { listHeight() {
return this.$refs.list.getBoundingClientRect().height; return this.$refs.list.getBoundingClientRect().height;
}, },
scrollHeight () { scrollHeight() {
return this.$refs.list.scrollHeight; return this.$refs.list.scrollHeight;
}, },
scrollTop () { scrollTop() {
return this.$refs.list.scrollTop + this.listHeight(); return this.$refs.list.scrollTop + this.listHeight();
}, },
loadNextPage () { loadNextPage() {
const getIssues = this.list.nextPage(); const getIssues = this.list.nextPage();
if (getIssues) { if (getIssues) {
...@@ -79,11 +68,40 @@ import boardCard from './board_card'; ...@@ -79,11 +68,40 @@ import boardCard from './board_card';
toggleForm() { toggleForm() {
this.showIssueForm = !this.showIssueForm; this.showIssueForm = !this.showIssueForm;
}, },
onScroll() {
if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
this.loadNextPage();
}
},
},
watch: {
filters: {
handler() {
this.list.loadingMore = false;
this.$refs.list.scrollTop = 0;
},
deep: true,
},
issues() {
this.$nextTick(() => {
if (this.scrollHeight() <= this.listHeight() &&
this.list.issuesSize > this.list.issues.length) {
this.list.page += 1;
this.list.getIssues(false);
}
if (this.scrollHeight() > Math.ceil(this.listHeight())) {
this.showCount = true;
} else {
this.showCount = false;
}
});
},
}, },
created() { created() {
gl.IssueBoardsApp.$on(`hide-issue-form-${this.list.id}`, this.toggleForm); eventHub.$on(`hide-issue-form-${this.list.id}`, this.toggleForm);
}, },
mounted () { mounted() {
const options = gl.issueBoards.getBoardSortableDefaultOptions({ const options = gl.issueBoards.getBoardSortableDefaultOptions({
scroll: document.querySelectorAll('.boards-list')[0], scroll: document.querySelectorAll('.boards-list')[0],
group: 'issues', group: 'issues',
...@@ -100,7 +118,8 @@ import boardCard from './board_card'; ...@@ -100,7 +118,8 @@ import boardCard from './board_card';
gl.issueBoards.onStart(); gl.issueBoards.onStart();
}, },
onAdd: (e) => { onAdd: (e) => {
gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex); gl.issueBoards.BoardsStore
.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex);
this.$nextTick(() => { this.$nextTick(() => {
e.item.remove(); e.item.remove();
...@@ -108,24 +127,71 @@ import boardCard from './board_card'; ...@@ -108,24 +127,71 @@ import boardCard from './board_card';
}, },
onUpdate: (e) => { onUpdate: (e) => {
const sortedArray = this.sortable.toArray().filter(id => id !== '-1'); const sortedArray = this.sortable.toArray().filter(id => id !== '-1');
gl.issueBoards.BoardsStore.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray); gl.issueBoards.BoardsStore
.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray);
}, },
onMove(e) { onMove(e) {
return !e.related.classList.contains('board-list-count'); return !e.related.classList.contains('board-list-count');
} },
}); });
this.sortable = Sortable.create(this.$refs.list, options); this.sortable = Sortable.create(this.$refs.list, options);
// Scroll event on list to load more // Scroll event on list to load more
this.$refs.list.onscroll = () => { this.$refs.list.addEventListener('scroll', this.onScroll);
if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
this.loadNextPage();
}
};
}, },
beforeDestroy() { beforeDestroy() {
gl.IssueBoardsApp.$off(`hide-issue-form-${this.list.id}`, this.toggleForm); eventHub.$off(`hide-issue-form-${this.list.id}`, this.toggleForm);
}, this.$refs.list.removeEventListener('scroll', this.onScroll);
}); },
})(); template: `
<div class="board-list-component">
<div
class="board-list-loading text-center"
aria-label="Loading issues"
v-if="loading">
<i
class="fa fa-spinner fa-spin"
aria-hidden="true">
</i>
</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-id="-1">
<i
class="fa fa-spinner fa-spin"
aria-label="Loading more issues"
aria-hidden="true"
v-show="list.loadingMore">
</i>
<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>
`,
};
/* global ListIssue */ /* global ListIssue */
import eventHub from '../eventhub';
const Store = gl.issueBoards.BoardsStore; const Store = gl.issueBoards.BoardsStore;
export default { export default {
...@@ -49,7 +51,7 @@ export default { ...@@ -49,7 +51,7 @@ export default {
}, },
cancel() { cancel() {
this.title = ''; this.title = '';
gl.IssueBoardsApp.$emit(`hide-issue-form-${this.list.id}`); eventHub.$emit(`hide-issue-form-${this.list.id}`);
}, },
}, },
mounted() { mounted() {
......
...@@ -24,6 +24,7 @@ export default Vue.component('environment-component', { ...@@ -24,6 +24,7 @@ export default Vue.component('environment-component', {
state: store.state, state: store.state,
visibility: 'available', visibility: 'available',
isLoading: false, isLoading: false,
isLoadingFolderContent: false,
cssContainerClass: environmentsData.cssClass, cssContainerClass: environmentsData.cssClass,
endpoint: environmentsData.environmentsDataEndpoint, endpoint: environmentsData.environmentsDataEndpoint,
canCreateDeployment: environmentsData.canCreateDeployment, canCreateDeployment: environmentsData.canCreateDeployment,
...@@ -68,15 +69,21 @@ export default Vue.component('environment-component', { ...@@ -68,15 +69,21 @@ export default Vue.component('environment-component', {
this.fetchEnvironments(); this.fetchEnvironments();
eventHub.$on('refreshEnvironments', this.fetchEnvironments); eventHub.$on('refreshEnvironments', this.fetchEnvironments);
eventHub.$on('toggleFolder', this.toggleFolder);
}, },
beforeDestroyed() { beforeDestroyed() {
eventHub.$off('refreshEnvironments'); eventHub.$off('refreshEnvironments');
eventHub.$off('toggleFolder');
}, },
methods: { methods: {
toggleRow(model) { toggleFolder(folder, folderUrl) {
return this.store.toggleFolder(model.name); this.store.toggleFolder(folder);
if (!folder.isOpen) {
this.fetchChildEnvironments(folder, folderUrl);
}
}, },
/** /**
...@@ -117,6 +124,21 @@ export default Vue.component('environment-component', { ...@@ -117,6 +124,21 @@ export default Vue.component('environment-component', {
new Flash('An error occurred while fetching the environments.'); new Flash('An error occurred while fetching the environments.');
}); });
}, },
fetchChildEnvironments(folder, folderUrl) {
this.isLoadingFolderContent = true;
this.service.getFolderContent(folderUrl)
.then(resp => resp.json())
.then((response) => {
this.store.setfolderContent(folder, response.environments);
this.isLoadingFolderContent = false;
})
.catch(() => {
this.isLoadingFolderContent = false;
new Flash('An error occurred while fetching the environments.');
});
},
}, },
template: ` template: `
...@@ -179,7 +201,8 @@ export default Vue.component('environment-component', { ...@@ -179,7 +201,8 @@ export default Vue.component('environment-component', {
:environments="state.environments" :environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed" :can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed" :can-read-environment="canReadEnvironmentParsed"
:service="service"/> :service="service"
:is-loading-folder-content="isLoadingFolderContent" />
</div> </div>
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1" <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
......
...@@ -45,11 +45,20 @@ export default { ...@@ -45,11 +45,20 @@ export default {
new Flash('An error occured while making the request.'); new Flash('An error occured while making the request.');
}); });
}, },
isActionDisabled(action) {
if (action.playable === undefined) {
return false;
}
return !action.playable;
},
}, },
template: ` template: `
<div class="btn-group" role="group"> <div class="btn-group" role="group">
<button <button
type="button"
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip" class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip"
data-container="body" data-container="body"
data-toggle="dropdown" data-toggle="dropdown"
...@@ -58,15 +67,23 @@ export default { ...@@ -58,15 +67,23 @@ export default {
:disabled="isLoading"> :disabled="isLoading">
<span> <span>
<span v-html="playIconSvg"></span> <span v-html="playIconSvg"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i> <i
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i> class="fa fa-caret-down"
aria-hidden="true"/>
<i
v-if="isLoading"
class="fa fa-spinner fa-spin"
aria-hidden="true"/>
</span> </span>
<ul class="dropdown-menu dropdown-menu-align-right"> <ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions"> <li v-for="action in actions">
<button <button
type="button"
class="js-manual-action-link no-btn btn"
@click="onClickAction(action.play_path)" @click="onClickAction(action.play_path)"
class="js-manual-action-link no-btn"> :class="{ 'disabled': isActionDisabled(action) }"
:disabled="isActionDisabled(action)">
${playIconSvg} ${playIconSvg}
<span> <span>
{{action.name}} {{action.name}}
......
...@@ -7,6 +7,7 @@ import RollbackComponent from './environment_rollback'; ...@@ -7,6 +7,7 @@ import RollbackComponent from './environment_rollback';
import TerminalButtonComponent from './environment_terminal_button'; import TerminalButtonComponent from './environment_terminal_button';
import MonitoringButtonComponent from './environment_monitoring'; import MonitoringButtonComponent from './environment_monitoring';
import CommitComponent from '../../vue_shared/components/commit'; import CommitComponent from '../../vue_shared/components/commit';
import eventHub from '../event_hub';
/** /**
* Envrionment Item Component * Envrionment Item Component
...@@ -141,6 +142,7 @@ export default { ...@@ -141,6 +142,7 @@ export default {
const parsedAction = { const parsedAction = {
name: gl.text.humanize(action.name), name: gl.text.humanize(action.name),
play_path: action.play_path, play_path: action.play_path,
playable: action.playable,
}; };
return parsedAction; return parsedAction;
}); });
...@@ -410,7 +412,6 @@ export default { ...@@ -410,7 +412,6 @@ export default {
folderUrl() { folderUrl() {
return `${window.location.pathname}/folders/${this.model.folderName}`; return `${window.location.pathname}/folders/${this.model.folderName}`;
}, },
}, },
/** /**
...@@ -428,15 +429,37 @@ export default { ...@@ -428,15 +429,37 @@ export default {
return true; return true;
}, },
methods: {
onClickFolder() {
eventHub.$emit('toggleFolder', this.model, this.folderUrl);
},
},
template: ` template: `
<tr> <tr :class="{ 'js-child-row': model.isChildren }">
<td> <td>
<a v-if="!model.isFolder" <a v-if="!model.isFolder"
class="environment-name" class="environment-name"
:class="{ 'prepend-left-default': model.isChildren }"
:href="environmentPath"> :href="environmentPath">
{{model.name}} {{model.name}}
</a> </a>
<a v-else class="folder-name" :href="folderUrl"> <span v-else
class="folder-name"
@click="onClickFolder"
role="button">
<span class="folder-icon">
<i
v-show="model.isOpen"
class="fa fa-caret-down"
aria-hidden="true" />
<i
v-show="!model.isOpen"
class="fa fa-caret-right"
aria-hidden="true"/>
</span>
<span class="folder-icon"> <span class="folder-icon">
<i class="fa fa-folder" aria-hidden="true"></i> <i class="fa fa-folder" aria-hidden="true"></i>
</span> </span>
...@@ -448,7 +471,7 @@ export default { ...@@ -448,7 +471,7 @@ export default {
<span class="badge"> <span class="badge">
{{model.size}} {{model.size}}
</span> </span>
</a> </span>
</td> </td>
<td class="deployment-column"> <td class="deployment-column">
......
...@@ -31,6 +31,18 @@ export default { ...@@ -31,6 +31,18 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
isLoadingFolderContent: {
type: Boolean,
required: false,
default: false,
},
},
methods: {
folderUrl(model) {
return `${window.location.pathname}/folders/${model.folderName}`;
},
}, },
template: ` template: `
...@@ -53,6 +65,31 @@ export default { ...@@ -53,6 +65,31 @@ export default {
:can-create-deployment="canCreateDeployment" :can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment" :can-read-environment="canReadEnvironment"
:service="service"></tr> :service="service"></tr>
<template v-if="model.isFolder && model.isOpen && model.children && model.children.length > 0">
<tr v-if="isLoadingFolderContent">
<td colspan="6" class="text-center">
<i class="fa fa-spin fa-spinner fa-2x" aria-hidden="true"/>
</td>
</tr>
<template v-else>
<tr is="environment-item"
v-for="children in model.children"
:model="children"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
:service="service"></tr>
<tr>
<td colspan="6" class="text-center">
<a :href="folderUrl(model)" class="btn btn-default">
Show all
</a>
</td>
</tr>
</template>
</template>
</template> </template>
</tbody> </tbody>
</table> </table>
......
...@@ -7,6 +7,7 @@ Vue.use(VueResource); ...@@ -7,6 +7,7 @@ Vue.use(VueResource);
export default class EnvironmentsService { export default class EnvironmentsService {
constructor(endpoint) { constructor(endpoint) {
this.environments = Vue.resource(endpoint); this.environments = Vue.resource(endpoint);
this.folderResults = 3;
} }
get(scope, page) { get(scope, page) {
...@@ -16,4 +17,8 @@ export default class EnvironmentsService { ...@@ -16,4 +17,8 @@ export default class EnvironmentsService {
postAction(endpoint) { postAction(endpoint) {
return Vue.http.post(endpoint, {}, { emulateJSON: true }); return Vue.http.post(endpoint, {}, { emulateJSON: true });
} }
getFolderContent(folderUrl) {
return Vue.http.get(`${folderUrl}.json?per_page=${this.folderResults}`);
}
} }
...@@ -38,7 +38,12 @@ export default class EnvironmentsStore { ...@@ -38,7 +38,12 @@ export default class EnvironmentsStore {
let filtered = {}; let filtered = {};
if (env.size > 1) { if (env.size > 1) {
filtered = Object.assign({}, env, { isFolder: true, folderName: env.name }); filtered = Object.assign({}, env, {
isFolder: true,
folderName: env.name,
isOpen: false,
children: [],
});
} }
if (env.latest) { if (env.latest) {
...@@ -85,4 +90,67 @@ export default class EnvironmentsStore { ...@@ -85,4 +90,67 @@ export default class EnvironmentsStore {
this.state.stoppedCounter = count; this.state.stoppedCounter = count;
return count; return count;
} }
/**
* Toggles folder open property for the given folder.
*
* @param {Object} folder
* @return {Array}
*/
toggleFolder(folder) {
return this.updateFolder(folder, 'isOpen', !folder.isOpen);
}
/**
* Updates the folder with the received environments.
*
*
* @param {Object} folder Folder to update
* @param {Array} environments Received environments
* @return {Object}
*/
setfolderContent(folder, environments) {
const updatedEnvironments = environments.map((env) => {
let updated = env;
if (env.latest) {
updated = Object.assign({}, env, env.latest);
delete updated.latest;
} else {
updated = env;
}
updated.isChildren = true;
return updated;
});
return this.updateFolder(folder, 'children', updatedEnvironments);
}
/**
* Given a folder a prop and a new value updates the correct folder.
*
* @param {Object} folder
* @param {String} prop
* @param {String|Boolean|Object|Array} newValue
* @return {Array}
*/
updateFolder(folder, prop, newValue) {
const environments = this.state.environments;
const updatedEnvironments = environments.map((env) => {
const updateEnv = Object.assign({}, env);
if (env.isFolder && env.id === folder.id) {
updateEnv[prop] = newValue;
}
return updateEnv;
});
this.state.environments = updatedEnvironments;
return updatedEnvironments;
}
} }
...@@ -45,14 +45,14 @@ window.GroupsSelect = (function() { ...@@ -45,14 +45,14 @@ window.GroupsSelect = (function() {
page, page,
per_page: GroupsSelect.PER_PAGE, per_page: GroupsSelect.PER_PAGE,
all_available, all_available,
skip_groups,
}; };
}, },
results: function (data, page) { results: function (data, page) {
if (data.length) return { results: [] }; if (data.length) return { results: [] };
const results = data.length ? data : data.results || []; const groups = data.length ? data : data.results || [];
const more = data.pagination ? data.pagination.more : false; const more = data.pagination ? data.pagination.more : false;
const results = groups.filter(group => skip_groups.indexOf(group.id) === -1);
return { return {
results, results,
......
import Vue from 'vue';
import IssueTitle from './issue_title';
import '../vue_shared/vue_resource_interceptor';
const vueOptions = () => ({
el: '.issue-title-entrypoint',
components: {
IssueTitle,
},
data() {
const issueTitleData = document.querySelector('.issue-title-data').dataset;
return {
initialTitle: issueTitleData.initialTitle,
endpoint: issueTitleData.endpoint,
};
},
template: `
<IssueTitle
:initialTitle="initialTitle"
:endpoint="endpoint"
/>
`,
});
(() => new Vue(vueOptions()))();
import Visibility from 'visibilityjs';
import Poll from './../lib/utils/poll';
import Service from './services/index';
export default {
props: {
initialTitle: { required: true, type: String },
endpoint: { required: true, type: String },
},
data() {
const resource = new Service(this.$http, this.endpoint);
const poll = new Poll({
resource,
method: 'getTitle',
successCallback: (res) => {
this.renderResponse(res);
},
errorCallback: (err) => {
if (process.env.NODE_ENV !== 'production') {
// eslint-disable-next-line no-console
console.error('ISSUE SHOW TITLE REALTIME ERROR', err);
} else {
throw new Error(err);
}
},
});
return {
poll,
timeoutId: null,
title: this.initialTitle,
};
},
methods: {
fetch() {
this.poll.makeRequest();
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
},
renderResponse(res) {
const body = JSON.parse(res.body);
this.triggerAnimation(body);
},
triggerAnimation(body) {
const { title } = body;
/**
* since opacity is changed, even if there is no diff for Vue to update
* we must check the title even on a 304 to ensure no visual change
*/
if (this.title === title) return;
this.$el.style.opacity = 0;
this.timeoutId = setTimeout(() => {
this.title = title;
this.$el.style.transition = 'opacity 0.2s ease';
this.$el.style.opacity = 1;
clearTimeout(this.timeoutId);
}, 100);
},
},
created() {
this.fetch();
},
template: `
<h2 class='title' v-html='title'></h2>
`,
};
export default class Service {
constructor(resource, endpoint) {
this.resource = resource;
this.endpoint = endpoint;
}
getTitle() {
return this.resource.get(this.endpoint);
}
}
/* eslint-disable import/prefer-default-export */
/**
* Function that allows a number with an X amount of decimals
* to be formatted in the following fashion:
* * For 1 digit to the left of the decimal point and X digits to the right of it
* * * Show 3 digits to the right
* * For 2 digits to the left of the decimal point and X digits to the right of it
* * * Show 2 digits to the right
*/
export function formatRelevantDigits(number) {
let digitsLeft = '';
let relevantDigits = 0;
let formattedNumber = '';
if (!isNaN(Number(number))) {
digitsLeft = number.split('.')[0];
switch (digitsLeft.length) {
case 1:
relevantDigits = 3;
break;
case 2:
relevantDigits = 2;
break;
case 3:
relevantDigits = 1;
break;
default:
relevantDigits = 4;
break;
}
formattedNumber = Number(number).toFixed(relevantDigits);
}
return formattedNumber;
}
...@@ -187,6 +187,9 @@ import './visibility_select'; ...@@ -187,6 +187,9 @@ import './visibility_select';
import './wikis'; import './wikis';
import './zen_mode'; import './zen_mode';
// eslint-disable-next-line global-require
if (process.env.NODE_ENV !== 'production') require('./test_utils/');
document.addEventListener('beforeunload', function () { document.addEventListener('beforeunload', function () {
// Unbind scroll events // Unbind scroll events
$(document).off('scroll'); $(document).off('scroll');
......
import simulateDrag from './simulate_drag';
// Export to global space for rspec to use
window.simulateDrag = simulateDrag;
/* eslint-disable wrap-iife, func-names, strict, no-var, vars-on-top, no-param-reassign, object-shorthand, no-shadow, comma-dangle, prefer-template, consistent-return, no-mixed-operators, no-unused-vars, no-unused-expressions, prefer-arrow-callback, max-len */ function simulateEvent(el, type, options = {}) {
(function () { let event;
'use strict'; if (!el) return null;
function simulateEvent(el, type, options) {
var event;
if (!el) return;
var ownerDocument = el.ownerDocument;
options = options || {};
if (/^mouse/.test(type)) { if (/^mouse/.test(type)) {
event = ownerDocument.createEvent('MouseEvents'); event = el.ownerDocument.createEvent('MouseEvents');
event.initMouseEvent(type, true, true, ownerDocument.defaultView, event.initMouseEvent(type, true, true, el.ownerDocument.defaultView,
options.button, options.screenX, options.screenY, options.clientX, options.clientY, options.button, options.screenX, options.screenY, options.clientX, options.clientY,
options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el); options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
} else { } else {
event = ownerDocument.createEvent('CustomEvent'); event = el.ownerDocument.createEvent('CustomEvent');
event.initCustomEvent(type, true, true, ownerDocument.defaultView, event.initCustomEvent(type, true, true, el.ownerDocument.defaultView,
options.button, options.screenX, options.screenY, options.clientX, options.clientY, options.button, options.screenX, options.screenY, options.clientX, options.clientY,
options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el); options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
event.dataTransfer = { event.dataTransfer = {
data: {}, data: {},
setData: function (type, val) { setData(key, val) {
this.data[type] = val; this.data[key] = val;
}, },
getData: function (type) { getData(key) {
return this.data[type]; return this.data[key];
} },
}; };
} }
if (el.dispatchEvent) { if (el.dispatchEvent) {
el.dispatchEvent(event); el.dispatchEvent(event);
} else if (el.fireEvent) { } else if (el.fireEvent) {
el.fireEvent('on' + type, event); el.fireEvent(`on${type}`, event);
} }
return event; return event;
} }
function isLast(target) { function isLast(target) {
var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el; const el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
var children = el.children; const children = el.children;
return children.length - 1 === target.index; return children.length - 1 === target.index;
} }
function getTarget(target) { function getTarget(target) {
var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el; const el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
var children = el.children; const children = el.children;
return ( return (
children[target.index] || children[target.index] ||
...@@ -60,49 +53,55 @@ ...@@ -60,49 +53,55 @@
children[target.index === 'last' ? children.length - 1 : -1] || children[target.index === 'last' ? children.length - 1 : -1] ||
el el
); );
} }
function getRect(el) { function getRect(el) {
var rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
var width = rect.right - rect.left; const width = rect.right - rect.left;
var height = rect.bottom - rect.top + 10; const height = (rect.bottom - rect.top) + 10;
return { return {
x: rect.left, x: rect.left,
y: rect.top, y: rect.top,
cx: rect.left + width / 2, cx: rect.left + (width / 2),
cy: rect.top + height / 2, cy: rect.top + (height / 2),
w: width, w: width,
h: height, h: height,
hw: width / 2, hw: width / 2,
wh: height / 2 wh: height / 2,
}; };
} }
function simulateDrag(options, callback) { export default function simulateDrag(options) {
options.to.el = options.to.el || options.from.el; const { to, from } = options;
to.el = to.el || from.el;
var fromEl = getTarget(options.from); const fromEl = getTarget(from);
var toEl = getTarget(options.to); const toEl = getTarget(to);
var firstEl = getTarget({ const firstEl = getTarget({
el: options.to.el, el: to.el,
index: 'first' index: 'first',
}); });
var lastEl = getTarget({ const lastEl = getTarget({
el: options.to.el, el: options.to.el,
index: 'last' index: 'last',
}); });
var scrollable = options.scrollable;
var fromRect = getRect(fromEl); const fromRect = getRect(fromEl);
var toRect = getRect(toEl); const toRect = getRect(toEl);
var firstRect = getRect(firstEl); const firstRect = getRect(firstEl);
var lastRect = getRect(lastEl); const lastRect = getRect(lastEl);
var startTime = new Date().getTime(); const startTime = new Date().getTime();
var duration = options.duration || 1000; const duration = options.duration || 1000;
simulateEvent(fromEl, 'mousedown', { button: 0 });
options.ontap && options.ontap(); simulateEvent(fromEl, 'mousedown', {
button: 0,
clientX: fromRect.cx,
clientY: fromRect.cy,
});
if (options.ontap) options.ontap();
window.SIMULATE_DRAG_ACTIVE = 1; window.SIMULATE_DRAG_ACTIVE = 1;
if (options.to.index === 0) { if (options.to.index === 0) {
...@@ -111,19 +110,19 @@ ...@@ -111,19 +110,19 @@
toRect.cy = lastRect.y + lastRect.h + 50; toRect.cy = lastRect.y + lastRect.h + 50;
} }
var dragInterval = setInterval(function loop() { const dragInterval = setInterval(() => {
var progress = (new Date().getTime() - startTime) / duration; const progress = (new Date().getTime() - startTime) / duration;
var x = (fromRect.cx + (toRect.cx - fromRect.cx) * progress) - scrollable.scrollLeft; const x = (fromRect.cx + ((toRect.cx - fromRect.cx) * progress));
var y = (fromRect.cy + (toRect.cy - fromRect.cy) * progress) - scrollable.scrollTop; const y = (fromRect.cy + ((toRect.cy - fromRect.cy) * progress));
var overEl = fromEl.ownerDocument.elementFromPoint(x, y); const overEl = fromEl.ownerDocument.elementFromPoint(x, y);
simulateEvent(overEl, 'mousemove', { simulateEvent(overEl, 'mousemove', {
clientX: x, clientX: x,
clientY: y clientY: y,
}); });
if (progress >= 1) { if (progress >= 1) {
options.ondragend && options.ondragend(); if (options.ondragend) options.ondragend();
simulateEvent(toEl, 'mouseup'); simulateEvent(toEl, 'mouseup');
clearInterval(dragInterval); clearInterval(dragInterval);
window.SIMULATE_DRAG_ACTIVE = 0; window.SIMULATE_DRAG_ACTIVE = 0;
...@@ -133,11 +132,6 @@ ...@@ -133,11 +132,6 @@
return { return {
target: fromEl, target: fromEl,
fromList: fromEl.parentNode, fromList: fromEl.parentNode,
toList: toEl.parentNode toList: toEl.parentNode,
}; };
} }
// Export
window.simulateEvent = simulateEvent;
window.simulateDrag = simulateDrag;
})();
...@@ -38,6 +38,14 @@ export default { ...@@ -38,6 +38,14 @@ export default {
new Flash('An error occured while making the request.'); new Flash('An error occured while making the request.');
}); });
}, },
isActionDisabled(action) {
if (action.playable === undefined) {
return false;
}
return !action.playable;
},
}, },
template: ` template: `
...@@ -51,16 +59,23 @@ export default { ...@@ -51,16 +59,23 @@ export default {
aria-label="Manual job" aria-label="Manual job"
:disabled="isLoading"> :disabled="isLoading">
${playIconSvg} ${playIconSvg}
<i class="fa fa-caret-down" aria-hidden="true"></i> <i
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i> class="fa fa-caret-down"
aria-hidden="true" />
<i
v-if="isLoading"
class="fa fa-spinner fa-spin"
aria-hidden="true" />
</button> </button>
<ul class="dropdown-menu dropdown-menu-align-right"> <ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions"> <li v-for="action in actions">
<button <button
type="button" type="button"
class="js-pipeline-action-link no-btn" class="js-pipeline-action-link no-btn btn"
@click="onClickAction(action.path)"> @click="onClickAction(action.path)"
:class="{ 'disabled': isActionDisabled(action) }"
:disabled="isActionDisabled(action)">
${playIconSvg} ${playIconSvg}
<span>{{action.name}}</span> <span>{{action.name}}</span>
</button> </button>
......
/* eslint-disable no-underscore-dangle*/ /* eslint-disable no-underscore-dangle*/
import '../../vue_realtime_listener'; import VueRealtimeListener from '../../vue_realtime_listener';
export default class PipelinesStore { export default class PipelinesStore {
constructor() { constructor() {
...@@ -56,6 +56,6 @@ export default class PipelinesStore { ...@@ -56,6 +56,6 @@ export default class PipelinesStore {
const removeIntervals = () => clearInterval(this.timeLoopInterval); const removeIntervals = () => clearInterval(this.timeLoopInterval);
const startIntervals = () => startTimeLoops(); const startIntervals = () => startTimeLoops();
gl.VueRealtimeListener(removeIntervals, startIntervals); VueRealtimeListener(removeIntervals, startIntervals);
} }
} }
/* eslint-disable no-param-reassign */ export default (removeIntervals, startIntervals) => {
((gl) => {
gl.VueRealtimeListener = (removeIntervals, startIntervals) => {
const removeAll = () => {
removeIntervals();
window.removeEventListener('beforeunload', removeIntervals);
window.removeEventListener('focus', startIntervals); window.removeEventListener('focus', startIntervals);
window.removeEventListener('blur', removeIntervals); window.removeEventListener('blur', removeIntervals);
document.removeEventListener('beforeunload', removeAll); window.removeEventListener('onbeforeload', removeIntervals);
};
window.addEventListener('beforeunload', removeIntervals);
window.addEventListener('focus', startIntervals); window.addEventListener('focus', startIntervals);
window.addEventListener('blur', removeIntervals); window.addEventListener('blur', removeIntervals);
document.addEventListener('beforeunload', removeAll); window.addEventListener('onbeforeload', removeIntervals);
};
// add removeAll methods to stack
const stack = gl.VueRealtimeListener.reset;
gl.VueRealtimeListener.reset = () => {
gl.VueRealtimeListener.reset = stack;
removeAll();
stack();
};
};
// remove all event listeners and intervals
gl.VueRealtimeListener.reset = () => undefined; // noop
})(window.gl || (window.gl = {}));
...@@ -292,6 +292,10 @@ ...@@ -292,6 +292,10 @@
} }
@media(min-width: $screen-xs-max) { @media(min-width: $screen-xs-max) {
&.merge-requests .text-content {
margin-top: 40px;
}
&.labels .text-content { &.labels .text-content {
margin-top: 70px; margin-top: 70px;
} }
......
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
} }
.issue-boards-page { .issue-boards-page {
.page-with-sidebar { .content-wrapper {
padding-bottom: 0; padding-bottom: 0;
} }
} }
...@@ -72,7 +72,7 @@ ...@@ -72,7 +72,7 @@
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
height: 475px; // Needed for PhantomJS height: 475px; // Needed for PhantomJS
height: calc(100vh - 220px); height: calc(100vh - 222px);
min-height: 475px; min-height: 475px;
transition: width .2s; transition: width .2s;
......
...@@ -159,6 +159,16 @@ ...@@ -159,6 +159,16 @@
text { text {
fill: $stat-graph-axis-fill; fill: $stat-graph-axis-fill;
} }
.label-axis-text,
.text-metric-usage {
fill: $black;
font-weight: 500;
}
.legend-axis-text {
fill: $black;
}
} }
.x-axis path, .x-axis path,
......
...@@ -459,20 +459,13 @@ a.deploy-project-label { ...@@ -459,20 +459,13 @@ a.deploy-project-label {
flex-wrap: wrap; flex-wrap: wrap;
.btn { .btn {
margin: 0 10px 10px 0;
padding: 8px; padding: 8px;
margin-left: 10px;
} }
> div { > div {
margin-bottom: 10px;
padding-left: 0; padding-left: 0;
&:last-child {
margin-bottom: 0;
.btn {
margin-right: 0;
}
}
} }
} }
} }
......
...@@ -7,6 +7,7 @@ module ContinueParams ...@@ -7,6 +7,7 @@ module ContinueParams
continue_params = continue_params.permit(:to, :notice, :notice_now) continue_params = continue_params.permit(:to, :notice, :notice_now)
return unless continue_params[:to] && continue_params[:to].start_with?('/') return unless continue_params[:to] && continue_params[:to].start_with?('/')
return if continue_params[:to].start_with?('//')
continue_params continue_params
end end
......
...@@ -15,6 +15,9 @@ module IssuableCollections ...@@ -15,6 +15,9 @@ module IssuableCollections
# a new order into the collection. # a new order into the collection.
# We cannot use reorder to not mess up the paginated collection. # We cannot use reorder to not mess up the paginated collection.
issuable_ids = issuable_collection.map(&:id) issuable_ids = issuable_collection.map(&:id)
return {} if issuable_ids.empty?
issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type) issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type)
issuable_votes_count = AwardEmoji.votes_for_collection(issuable_ids, @collection_type) issuable_votes_count = AwardEmoji.votes_for_collection(issuable_ids, @collection_type)
issuable_merge_requests_count = issuable_merge_requests_count =
......
...@@ -7,7 +7,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController ...@@ -7,7 +7,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
@sort = params[:sort] @sort = params[:sort]
@todos = @todos.page(params[:page]) @todos = @todos.page(params[:page])
if @todos.out_of_range? && @todos.total_pages != 0 if @todos.out_of_range? && @todos.total_pages != 0
redirect_to url_for(params.merge(page: @todos.total_pages)) redirect_to url_for(params.merge(page: @todos.total_pages, only_path: true))
end end
end end
......
...@@ -10,6 +10,7 @@ class Groups::ApplicationController < ApplicationController ...@@ -10,6 +10,7 @@ class Groups::ApplicationController < ApplicationController
unless @group unless @group
id = params[:group_id] || params[:id] id = params[:group_id] || params[:id]
@group = Group.find_by_full_path(id) @group = Group.find_by_full_path(id)
@group_merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id).execute
unless @group && can?(current_user, :read_group, @group) unless @group && can?(current_user, :read_group, @group)
@group = nil @group = nil
......
class Import::BaseController < ApplicationController class Import::BaseController < ApplicationController
private private
def find_or_create_namespace(name, owner) def find_or_create_namespace(names, owner)
return current_user.namespace if name == owner return current_user.namespace if names == owner
return current_user.namespace unless current_user.can_create_group? return current_user.namespace unless current_user.can_create_group?
names = params[:target_namespace].presence || names
full_path_namespace = Namespace.find_by_full_path(names)
return full_path_namespace if full_path_namespace
names.split('/').inject(nil) do |parent, name|
begin begin
name = params[:target_namespace].presence || name namespace = Group.create!(name: name,
namespace = Group.create!(name: name, path: name, owner: current_user) path: name,
owner: current_user,
parent: parent)
namespace.add_owner(current_user) namespace.add_owner(current_user)
namespace namespace
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
Namespace.find_by_full_path(name) Namespace.where(parent: parent).find_by_path_or_name(name)
end
end end
end end
end end
...@@ -11,10 +11,10 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -11,10 +11,10 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :redirect_to_external_issue_tracker, only: [:index, :new] before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :module_enabled before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests, before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
:related_branches, :can_create_branch] :related_branches, :can_create_branch, :rendered_title]
# Allow read any issue # Allow read any issue
before_action :authorize_read_issue!, only: [:show] before_action :authorize_read_issue!, only: [:show, :rendered_title]
# Allow write(create) issue # Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create] before_action :authorize_create_issue!, only: [:new, :create]
...@@ -31,7 +31,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -31,7 +31,7 @@ class Projects::IssuesController < Projects::ApplicationController
@issuable_meta_data = issuable_meta_data(@issues, @collection_type) @issuable_meta_data = issuable_meta_data(@issues, @collection_type)
if @issues.out_of_range? && @issues.total_pages != 0 if @issues.out_of_range? && @issues.total_pages != 0
return redirect_to url_for(params.merge(page: @issues.total_pages)) return redirect_to url_for(params.merge(page: @issues.total_pages, only_path: true))
end end
if params[:label_name].present? if params[:label_name].present?
...@@ -200,6 +200,11 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -200,6 +200,11 @@ class Projects::IssuesController < Projects::ApplicationController
end end
end end
def rendered_title
Gitlab::PollingInterval.set_header(response, interval: 3_000)
render json: { title: view_context.markdown_field(@issue, :title) }
end
protected protected
def issue def issue
......
...@@ -43,7 +43,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -43,7 +43,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type) @issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
if @merge_requests.out_of_range? && @merge_requests.total_pages != 0 if @merge_requests.out_of_range? && @merge_requests.total_pages != 0
return redirect_to url_for(params.merge(page: @merge_requests.total_pages)) return redirect_to url_for(params.merge(page: @merge_requests.total_pages, only_path: true))
end end
if params[:label_name].present? if params[:label_name].present?
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
# state: 'open' or 'closed' or 'all' # state: 'open' or 'closed' or 'all'
# group_id: integer # group_id: integer
# project_id: integer # project_id: integer
# milestone_id: integer # milestone_title: string
# assignee_id: integer # assignee_id: integer
# search: string # search: string
# label_name: string # label_name: string
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
# state: 'open' or 'closed' or 'all' # state: 'open' or 'closed' or 'all'
# group_id: integer # group_id: integer
# project_id: integer # project_id: integer
# milestone_id: integer # milestone_title: string
# assignee_id: integer # assignee_id: integer
# search: string # search: string
# label_name: string # label_name: string
......
...@@ -407,7 +407,10 @@ module ProjectsHelper ...@@ -407,7 +407,10 @@ module ProjectsHelper
def sanitize_repo_path(project, message) def sanitize_repo_path(project, message)
return '' unless message.present? return '' unless message.present?
message.strip.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]") exports_path = File.join(Settings.shared['path'], 'tmp/project_exports')
filtered_message = message.strip.gsub(exports_path, "[REPO EXPORT PATH]")
filtered_message.gsub(project.repository_storage_path.chomp('/'), "[REPOS PATH]")
end end
def project_feature_options def project_feature_options
......
...@@ -46,10 +46,18 @@ class Blob < SimpleDelegator ...@@ -46,10 +46,18 @@ class Blob < SimpleDelegator
text? && language && language.name == 'SVG' text? && language && language.name == 'SVG'
end end
def pdf?
name && File.extname(name) == '.pdf'
end
def ipython_notebook? def ipython_notebook?
text? && language&.name == 'Jupyter Notebook' text? && language&.name == 'Jupyter Notebook'
end end
def sketch?
binary? && extname.downcase.delete('.') == 'sketch'
end
def size_within_svg_limits? def size_within_svg_limits?
size <= MAXIMUM_SVG_SIZE size <= MAXIMUM_SVG_SIZE
end end
...@@ -67,8 +75,12 @@ class Blob < SimpleDelegator ...@@ -67,8 +75,12 @@ class Blob < SimpleDelegator
end end
elsif image? || svg? elsif image? || svg?
'image' 'image'
elsif pdf?
'pdf'
elsif ipython_notebook? elsif ipython_notebook?
'notebook' 'notebook'
elsif sketch?
'sketch'
elsif text? elsif text?
'text' 'text'
else else
......
...@@ -540,6 +540,8 @@ module Ci ...@@ -540,6 +540,8 @@ module Ci
end end
def dependencies def dependencies
return [] if empty_dependencies?
depended_jobs = depends_on_builds depended_jobs = depends_on_builds
return depended_jobs unless options[:dependencies].present? return depended_jobs unless options[:dependencies].present?
...@@ -549,6 +551,10 @@ module Ci ...@@ -549,6 +551,10 @@ module Ci
end end
end end
def empty_dependencies?
options[:dependencies]&.empty?
end
private private
def update_artifacts_size def update_artifacts_size
......
...@@ -40,6 +40,8 @@ class Issue < ActiveRecord::Base ...@@ -40,6 +40,8 @@ class Issue < ActiveRecord::Base
scope :include_associations, -> { includes(:assignee, :labels, project: :namespace) } scope :include_associations, -> { includes(:assignee, :labels, project: :namespace) }
after_save :expire_etag_cache
attr_spammable :title, spam_title: true attr_spammable :title, spam_title: true
attr_spammable :description, spam_description: true attr_spammable :description, spam_description: true
...@@ -59,10 +61,6 @@ class Issue < ActiveRecord::Base ...@@ -59,10 +61,6 @@ class Issue < ActiveRecord::Base
before_transition any => :closed do |issue| before_transition any => :closed do |issue|
issue.closed_at = Time.zone.now issue.closed_at = Time.zone.now
end end
before_transition closed: any do |issue|
issue.closed_at = nil
end
end end
def hook_attrs def hook_attrs
...@@ -256,4 +254,13 @@ class Issue < ActiveRecord::Base ...@@ -256,4 +254,13 @@ class Issue < ActiveRecord::Base
def publicly_visible? def publicly_visible?
project.public? && !confidential? project.public? && !confidential?
end end
def expire_etag_cache
key = Gitlab::Routing.url_helpers.rendered_title_namespace_project_issue_path(
project.namespace,
project,
self
)
Gitlab::EtagCaching::Store.new.touch(key)
end
end end
...@@ -177,6 +177,16 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -177,6 +177,16 @@ class MergeRequestDiff < ActiveRecord::Base
st_commits.count st_commits.count
end end
def utf8_st_diffs
return [] if st_diffs.blank?
st_diffs.map do |diff|
diff.each do |k, v|
diff[k] = encode_utf8(v) if v.respond_to?(:encoding)
end
end
end
private private
# Old GitLab implementations may have generated diffs as ["--broken-diff"]. # Old GitLab implementations may have generated diffs as ["--broken-diff"].
...@@ -270,14 +280,6 @@ class MergeRequestDiff < ActiveRecord::Base ...@@ -270,14 +280,6 @@ class MergeRequestDiff < ActiveRecord::Base
project.merge_base_commit(head_commit_sha, start_commit_sha).try(:sha) project.merge_base_commit(head_commit_sha, start_commit_sha).try(:sha)
end end
def utf8_st_diffs
st_diffs.map do |diff|
diff.each do |k, v|
diff[k] = encode_utf8(v) if v.respond_to?(:encoding)
end
end
end
# #
# #save or #update_attributes providing changes on serialized attributes do a lot of # #save or #update_attributes providing changes on serialized attributes do a lot of
# serialization and deserialization calls resulting in bad performance. # serialization and deserialization calls resulting in bad performance.
......
...@@ -150,7 +150,7 @@ class Namespace < ActiveRecord::Base ...@@ -150,7 +150,7 @@ class Namespace < ActiveRecord::Base
end end
def any_project_has_container_registry_tags? def any_project_has_container_registry_tags?
projects.any?(&:has_container_registry_tags?) all_projects.any?(&:has_container_registry_tags?)
end end
def send_update_instructions def send_update_instructions
...@@ -214,6 +214,12 @@ class Namespace < ActiveRecord::Base ...@@ -214,6 +214,12 @@ class Namespace < ActiveRecord::Base
@old_repository_storage_paths ||= repository_storage_paths @old_repository_storage_paths ||= repository_storage_paths
end end
# Includes projects from this namespace and projects from all subgroups
# that belongs to this namespace
def all_projects
Project.inside_path(full_path)
end
private private
def repository_storage_paths def repository_storage_paths
...@@ -221,7 +227,7 @@ class Namespace < ActiveRecord::Base ...@@ -221,7 +227,7 @@ class Namespace < ActiveRecord::Base
# pending delete. Unscoping also get rids of the default order, which causes # pending delete. Unscoping also get rids of the default order, which causes
# problems with SELECT DISTINCT. # problems with SELECT DISTINCT.
Project.unscoped do Project.unscoped do
projects.select('distinct(repository_storage)').to_a.map(&:repository_storage_path) all_projects.select('distinct(repository_storage)').to_a.map(&:repository_storage_path)
end end
end end
......
...@@ -114,6 +114,8 @@ class Project < ActiveRecord::Base ...@@ -114,6 +114,8 @@ class Project < ActiveRecord::Base
has_one :kubernetes_service, dependent: :destroy, inverse_of: :project has_one :kubernetes_service, dependent: :destroy, inverse_of: :project
has_one :prometheus_service, dependent: :destroy, inverse_of: :project has_one :prometheus_service, dependent: :destroy, inverse_of: :project
has_one :mock_ci_service, dependent: :destroy has_one :mock_ci_service, dependent: :destroy
has_one :mock_deployment_service, dependent: :destroy
has_one :mock_monitoring_service, dependent: :destroy
has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id" has_one :forked_project_link, dependent: :destroy, foreign_key: "forked_to_project_id"
has_one :forked_from_project, through: :forked_project_link has_one :forked_from_project, through: :forked_project_link
......
...@@ -22,22 +22,21 @@ class KubernetesService < DeploymentService ...@@ -22,22 +22,21 @@ class KubernetesService < DeploymentService
with_options presence: true, if: :activated? do with_options presence: true, if: :activated? do
validates :api_url, url: true validates :api_url, url: true
validates :token validates :token
end
validates :namespace, validates :namespace,
allow_blank: true,
length: 1..63,
if: :activated?,
format: { format: {
with: Gitlab::Regex.kubernetes_namespace_regex, with: Gitlab::Regex.kubernetes_namespace_regex,
message: Gitlab::Regex.kubernetes_namespace_regex_message, message: Gitlab::Regex.kubernetes_namespace_regex_message
}, }
length: 1..63
end
after_save :clear_reactive_cache! after_save :clear_reactive_cache!
def initialize_properties def initialize_properties
if properties.nil? self.properties = {} if properties.nil?
self.properties = {}
self.namespace = "#{project.path}-#{project.id}" if project.present?
end
end end
def title def title
...@@ -62,7 +61,7 @@ class KubernetesService < DeploymentService ...@@ -62,7 +61,7 @@ class KubernetesService < DeploymentService
{ type: 'text', { type: 'text',
name: 'namespace', name: 'namespace',
title: 'Kubernetes namespace', title: 'Kubernetes namespace',
placeholder: 'Kubernetes namespace' }, placeholder: namespace_placeholder },
{ type: 'text', { type: 'text',
name: 'api_url', name: 'api_url',
title: 'API URL', title: 'API URL',
...@@ -92,7 +91,7 @@ class KubernetesService < DeploymentService ...@@ -92,7 +91,7 @@ class KubernetesService < DeploymentService
variables = [ variables = [
{ key: 'KUBE_URL', value: api_url, public: true }, { key: 'KUBE_URL', value: api_url, public: true },
{ key: 'KUBE_TOKEN', value: token, public: false }, { key: 'KUBE_TOKEN', value: token, public: false },
{ key: 'KUBE_NAMESPACE', value: namespace, public: true } { key: 'KUBE_NAMESPACE', value: namespace_variable, public: true }
] ]
if ca_pem.present? if ca_pem.present?
...@@ -135,8 +134,26 @@ class KubernetesService < DeploymentService ...@@ -135,8 +134,26 @@ class KubernetesService < DeploymentService
{ pods: pods } { pods: pods }
end end
TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze
private private
def namespace_placeholder
default_namespace || TEMPLATE_PLACEHOLDER
end
def namespace_variable
if namespace.present?
namespace
else
default_namespace
end
end
def default_namespace
"#{project.path}-#{project.id}" if project.present?
end
def build_kubeclient!(api_path: 'api', api_version: 'v1') def build_kubeclient!(api_path: 'api', api_version: 'v1')
raise "Incomplete settings" unless api_url && namespace && token raise "Incomplete settings" unless api_url && namespace && token
......
class MockDeploymentService < DeploymentService
def title
'Mock deployment'
end
def description
'Mock deployment service'
end
def self.to_param
'mock_deployment'
end
# No terminals support
def terminals(environment)
[]
end
end
class MockMonitoringService < MonitoringService
def title
'Mock monitoring'
end
def description
'Mock monitoring service'
end
def self.to_param
'mock_monitoring'
end
def metrics(environment)
JSON.parse(File.read(Rails.root + 'spec/fixtures/metrics.json'))
end
end
...@@ -59,7 +59,7 @@ class Repository ...@@ -59,7 +59,7 @@ class Repository
def raw_repository def raw_repository
return nil unless path_with_namespace return nil unless path_with_namespace
@raw_repository ||= Gitlab::Git::Repository.new(path_to_repo) @raw_repository ||= initialize_raw_repository
end end
# Return absolute path to repository # Return absolute path to repository
...@@ -146,12 +146,7 @@ class Repository ...@@ -146,12 +146,7 @@ class Repository
# may cause the branch to "disappear" erroneously or have the wrong SHA. # may cause the branch to "disappear" erroneously or have the wrong SHA.
# #
# See: https://github.com/libgit2/libgit2/issues/1534 and https://gitlab.com/gitlab-org/gitlab-ce/issues/15392 # See: https://github.com/libgit2/libgit2/issues/1534 and https://gitlab.com/gitlab-org/gitlab-ce/issues/15392
raw_repo = raw_repo = fresh_repo ? initialize_raw_repository : raw_repository
if fresh_repo
Gitlab::Git::Repository.new(path_to_repo)
else
raw_repository
end
raw_repo.find_branch(name) raw_repo.find_branch(name)
end end
...@@ -505,9 +500,7 @@ class Repository ...@@ -505,9 +500,7 @@ class Repository
end end
end end
def branch_names delegate :branch_names, to: :raw_repository
branches.map(&:name)
end
cache_method :branch_names, fallback: [] cache_method :branch_names, fallback: []
delegate :tag_names, to: :raw_repository delegate :tag_names, to: :raw_repository
...@@ -1168,4 +1161,8 @@ class Repository ...@@ -1168,4 +1161,8 @@ class Repository
def repository_storage_path def repository_storage_path
@project.repository_storage_path @project.repository_storage_path
end end
def initialize_raw_repository
Gitlab::Git::Repository.new(project.repository_storage, path_with_namespace + '.git')
end
end end
...@@ -238,7 +238,9 @@ class Service < ActiveRecord::Base ...@@ -238,7 +238,9 @@ class Service < ActiveRecord::Base
slack slack
teamcity teamcity
] ]
service_names << 'mock_ci' if Rails.env.development? if Rails.env.development?
service_names += %w[mock_ci mock_deployment mock_monitoring]
end
service_names.sort_by(&:downcase) service_names.sort_by(&:downcase)
end end
......
class SystemNoteMetadata < ActiveRecord::Base class SystemNoteMetadata < ActiveRecord::Base
ICON_TYPES = %w[ ICON_TYPES = %w[
commit merge confidentiality status label assignee cross_reference commit merge confidential visible label assignee cross_reference
title time_tracking branch milestone discussion task moved title time_tracking branch milestone discussion task moved opened closed merged
].freeze ].freeze
validates :note, presence: true validates :note, presence: true
......
...@@ -11,4 +11,6 @@ class BuildActionEntity < Grape::Entity ...@@ -11,4 +11,6 @@ class BuildActionEntity < Grape::Entity
build.project, build.project,
build) build)
end end
expose :playable?, as: :playable
end end
...@@ -16,6 +16,7 @@ class BuildEntity < Grape::Entity ...@@ -16,6 +16,7 @@ class BuildEntity < Grape::Entity
path_to(:play_namespace_project_build, build) path_to(:play_namespace_project_build, build)
end end
expose :playable?, as: :playable
expose :created_at expose :created_at
expose :updated_at expose :updated_at
expose :detailed_status, as: :status, with: StatusEntity expose :detailed_status, as: :status, with: StatusEntity
......
...@@ -21,7 +21,9 @@ module MergeRequests ...@@ -21,7 +21,9 @@ module MergeRequests
delegate :target_branch, :source_branch, :source_project, :target_project, :compare_commits, :wip_title, :description, :errors, to: :merge_request delegate :target_branch, :source_branch, :source_project, :target_project, :compare_commits, :wip_title, :description, :errors, to: :merge_request
def find_source_project def find_source_project
source_project || project return source_project if source_project.present? && can?(current_user, :read_project, source_project)
project
end end
def find_target_project def find_target_project
......
...@@ -18,7 +18,11 @@ module Search ...@@ -18,7 +18,11 @@ module Search
end end
def scope def scope
@scope ||= %w[issues merge_requests milestones].delete(params[:scope]) { 'projects' } @scope ||= begin
allowed_scopes = %w[issues merge_requests milestones]
allowed_scopes.delete(params[:scope]) { 'projects' }
end
end end
end end
end end
...@@ -183,7 +183,9 @@ module SystemNoteService ...@@ -183,7 +183,9 @@ module SystemNoteService
body = status.dup body = status.dup
body << " via #{source.gfm_reference(project)}" if source body << " via #{source.gfm_reference(project)}" if source
create_note(NoteSummary.new(noteable, project, author, body, action: 'status')) action = status == 'reopened' ? 'opened' : status
create_note(NoteSummary.new(noteable, project, author, body, action: action))
end end
# Called when 'merge when pipeline succeeds' is executed # Called when 'merge when pipeline succeeds' is executed
...@@ -273,9 +275,15 @@ module SystemNoteService ...@@ -273,9 +275,15 @@ module SystemNoteService
# #
# Returns the created Note object # Returns the created Note object
def change_issue_confidentiality(issue, project, author) def change_issue_confidentiality(issue, project, author)
body = issue.confidential ? 'made the issue confidential' : 'made the issue visible to everyone' if issue.confidential
body = 'made the issue confidential'
action = 'confidential'
else
body = 'made the issue visible to everyone'
action = 'visible'
end
create_note(NoteSummary.new(issue, project, author, body, action: 'confidentiality')) create_note(NoteSummary.new(issue, project, author, body, action: action))
end end
# Called when a branch in Noteable is changed # Called when a branch in Noteable is changed
......
...@@ -62,6 +62,7 @@ module Users ...@@ -62,6 +62,7 @@ module Users
:email, :email,
:external, :external,
:force_random_password, :force_random_password,
:password_automatically_set,
:hide_no_password, :hide_no_password,
:hide_no_ssh_key, :hide_no_ssh_key,
:key_id, :key_id,
...@@ -85,6 +86,7 @@ module Users ...@@ -85,6 +86,7 @@ module Users
[ [
:email, :email,
:email_confirmation, :email_confirmation,
:password_automatically_set,
:name, :name,
:password, :password,
:username :username
......
- page_title "Merge Requests" - page_title "Merge Requests"
.top-area - if @group_merge_requests.empty?
= render 'shared/empty_states/merge_requests', project_select_button: true
- else
.top-area
= render 'shared/issuable/nav', type: :merge_requests = render 'shared/issuable/nav', type: :merge_requests
- if current_user - if current_user
.nav-controls .nav-controls
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request" = render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New merge request"
= render 'shared/issuable/filter', type: :merge_requests = render 'shared/issuable/filter', type: :merge_requests
.row-content-block.second-block .row-content-block.second-block
Only merge requests from Only merge requests from
%strong= @group.name %strong= @group.name
group are listed here. group are listed here.
- if current_user - if current_user
To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page. To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page.
= render 'shared/merge_requests' .prepend-top-default
= render 'shared/merge_requests'
= render "header_title" = render "header_title"
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
= render 'shared/milestones/top', milestone: @milestone, group: @group = render 'shared/milestones/top', milestone: @milestone, group: @group
= render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true = render 'shared/milestones/tabs', milestone: @milestone, show_project_name: true
= render 'shared/milestones/sidebar', milestone: @milestone, affix_offset: 102 = render 'shared/milestones/sidebar', milestone: @milestone, affix_offset: 102
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('pdf_viewer')
.file-content#js-pdf-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } }
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('sketch_viewer')
.file-content#js-sketch-viewer{ data: { endpoint: namespace_project_raw_path(@project.namespace, @project, @id) } }
.js-loading-icon.text-center.prepend-top-default.append-bottom-default.js-loading-icon{ 'aria-label' => 'Loading Sketch preview' }
= icon('spinner spin 2x', 'aria-hidden' => 'true');
...@@ -6,10 +6,8 @@ ...@@ -6,10 +6,8 @@
= page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag('filtered_search') = page_specific_javascript_bundle_tag('filtered_search')
= page_specific_javascript_bundle_tag('boards') = page_specific_javascript_bundle_tag('boards')
= page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
%script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board" %script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board"
%script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list"
%script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal %script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
= render "projects/issues/head" = render "projects/issues/head"
......
.board-list-component
.board-list-loading.text-center{ "v-if" => "loading" }
= icon("spinner spin")
- if can? current_user, :create_issue, @project
%board-new-issue{ ":list" => "list",
"v-if" => 'list.type !== "closed" && showIssueForm' }
%ul.board-list{ "ref" => "list",
"v-show" => "!loading",
":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.board-list-count.text-center{ "v-if" => "showCount",
"data-issue-id" => "-1" }
= icon("spinner spin", "v-show" => "list.loadingMore" )
%span{ "v-if" => "list.issues.length === list.issuesSize" }
Showing all issues
%span{ "v-else" => true }
Showing {{ list.issues.length }} of {{ list.issuesSize }} issues
...@@ -238,6 +238,8 @@ ...@@ -238,6 +238,8 @@
%ul %ul
%li Be careful. Renaming a project's repository can have unintended side effects. %li Be careful. Renaming a project's repository can have unintended side effects.
%li You will need to update your local repositories to point to the new location. %li You will need to update your local repositories to point to the new location.
- if @project.deployment_services.any?
%li Your deployment services will be broken, you will need to manually fix the services after renaming.
= f.submit 'Rename project', class: "btn btn-warning" = f.submit 'Rename project', class: "btn btn-warning"
- if can?(current_user, :change_namespace, @project) - if can?(current_user, :change_namespace, @project)
%hr %hr
......
- if environment.external_url && can?(current_user, :read_environment, environment) - if environment.external_url && can?(current_user, :read_environment, environment)
= link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url' do = link_to environment.external_url, target: '_blank', rel: 'noopener noreferrer', class: 'btn external-url' do
= icon('external-link') = icon('external-link')
View deployment
...@@ -4,3 +4,4 @@ ...@@ -4,3 +4,4 @@
= link_to environment_metrics_path(environment), title: 'See metrics', class: 'btn metrics-button' do = link_to environment_metrics_path(environment), title: 'See metrics', class: 'btn metrics-button' do
= icon('area-chart') = icon('area-chart')
Monitoring
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
.col-sm-6 .col-sm-6
%h3.page-title %h3.page-title
Environment: Environment:
= @environment.name = link_to @environment.name, environment_path(@environment)
.col-sm-6 .col-sm-6
.nav-controls .nav-controls
......
...@@ -4,9 +4,9 @@ ...@@ -4,9 +4,9 @@
%div{ class: container_class } %div{ class: container_class }
.top-area.adjust .top-area.adjust
.col-md-9 .col-md-7
%h3.page-title= @environment.name %h3.page-title= @environment.name
.col-md-3 .col-md-5
.nav-controls .nav-controls
= render 'projects/environments/metrics_button', environment: @environment = render 'projects/environments/metrics_button', environment: @environment
= render 'projects/environments/terminal_button', environment: @environment = render 'projects/environments/terminal_button', environment: @environment
......
...@@ -49,11 +49,12 @@ ...@@ -49,11 +49,12 @@
= link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam' = link_to 'Submit as spam', mark_as_spam_namespace_project_issue_path(@project.namespace, @project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
= link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit' = link_to 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
.issue-details.issuable-details .issue-details.issuable-details
.detail-page-description.content-block{ class: ('hide-bottom-border' unless @issue.description.present? ) } .detail-page-description.content-block{ class: ('hide-bottom-border' unless @issue.description.present? ) }
%h2.title .issue-title-data.hidden{ "data" => { "initial-title" => markdown_field(@issue, :title),
= markdown_field(@issue, :title) "endpoint" => rendered_title_namespace_project_issue_path(@project.namespace, @project, @issue),
} }
.issue-title-entrypoint
- if @issue.description.present? - if @issue.description.present?
.description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' } .description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' }
.wiki .wiki
...@@ -77,3 +78,5 @@ ...@@ -77,3 +78,5 @@
= render 'projects/issues/discussion' = render 'projects/issues/discussion'
= render 'shared/issuable/sidebar', issuable: @issue = render 'shared/issuable/sidebar', issuable: @issue
= page_specific_javascript_bundle_tag('issue_show')
...@@ -3,9 +3,6 @@ ...@@ -3,9 +3,6 @@
- hide_class = '' - hide_class = ''
= render "projects/issues/head" = render "projects/issues/head"
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
- if @labels.exists? || @prioritized_labels.exists? - if @labels.exists? || @prioritized_labels.exists?
%div{ class: container_class } %div{ class: container_class }
.top-area.adjust .top-area.adjust
......
%ul.content-list.mr-list.issuable-list %ul.content-list.mr-list.issuable-list
- if @merge_requests.exists?
= render @merge_requests = render @merge_requests
- if @merge_requests.blank? - else
%li = render 'shared/empty_states/merge_requests'
.nothing-here-block No merge requests to show
- if @merge_requests.present? - if @merge_requests.present?
= paginate @merge_requests, theme: "gitlab" = paginate @merge_requests, theme: "gitlab"
...@@ -7,16 +7,19 @@ ...@@ -7,16 +7,19 @@
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('filtered_search') = page_specific_javascript_bundle_tag('filtered_search')
%div{ class: container_class } - if @project.merge_requests.exists?
%div{ class: container_class }
.top-area .top-area
= render 'shared/issuable/nav', type: :merge_requests = render 'shared/issuable/nav', type: :merge_requests
.nav-controls .nav-controls
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project)) - merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
- if merge_project - if merge_project
= link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New Merge Request" do = link_to new_namespace_project_merge_request_path(merge_project.namespace, merge_project), class: "btn btn-new", title: "New merge request" do
New Merge Request New merge request
= render 'shared/issuable/search_bar', type: :merge_requests = render 'shared/issuable/search_bar', type: :merge_requests
.merge-requests-holder .merge-requests-holder
= render 'merge_requests' = render 'merge_requests'
- else
= render 'shared/empty_states/merge_requests', button_path: new_namespace_project_merge_request_path(@project.namespace, @project)
...@@ -3,9 +3,6 @@ ...@@ -3,9 +3,6 @@
- page_description @milestone.description - page_description @milestone.description
= render "projects/issues/head" = render "projects/issues/head"
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('simulate_drag') if Rails.env.test?
%div{ class: container_class } %div{ class: container_class }
.detail-page-header.milestone-page-header .detail-page-header.milestone-page-header
.status-box{ class: status_box_class(@milestone) } .status-box{ class: status_box_class(@milestone) }
......
...@@ -78,7 +78,7 @@ ...@@ -78,7 +78,7 @@
- if git_import_enabled? - if git_import_enabled?
%button.btn.js-toggle-button.import_git{ type: "button" } %button.btn.js-toggle-button.import_git{ type: "button" }
= icon('git', text: 'Repo by URL') = icon('git', text: 'Repo by URL')
.import_gitlab_project .import_gitlab_project.has-tooltip{ data: { container: 'body' } }
- if gitlab_project_import_enabled? - if gitlab_project_import_enabled?
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do = link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
= icon('gitlab', text: 'GitLab export') = icon('gitlab', text: 'GitLab export')
...@@ -109,6 +109,9 @@ ...@@ -109,6 +109,9 @@
%p Please wait a moment, this page will automatically refresh when ready. %p Please wait a moment, this page will automatically refresh when ready.
:javascript :javascript
var importBtnTooltip = "Please enter a valid project name.";
var $importBtnWrapper = $('.import_gitlab_project');
$('.how_to_import_link').bind('click', function (e) { $('.how_to_import_link').bind('click', function (e) {
e.preventDefault(); e.preventDefault();
var import_modal = $(this).next(".modal").show(); var import_modal = $(this).next(".modal").show();
...@@ -123,15 +126,8 @@ ...@@ -123,15 +126,8 @@
$(".btn_import_gitlab_project").attr("href", _href + '?namespace_id=' + $("#project_namespace_id").val() + '&path=' + $("#project_path").val()); $(".btn_import_gitlab_project").attr("href", _href + '?namespace_id=' + $("#project_namespace_id").val() + '&path=' + $("#project_path").val());
}); });
$('.btn_import_gitlab_project').attr('disabled',true) $('.btn_import_gitlab_project').attr('disabled', $('#project_path').val().trim().length === 0);
$('.import_gitlab_project').attr('title', 'Project path and name required.'); $importBtnWrapper.attr('title', importBtnTooltip);
$('.import_gitlab_project').click(function( event ) {
if($('.btn_import_gitlab_project').attr('disabled')) {
event.preventDefault();
new Flash("Please enter path and name for the project to be imported to.");
}
});
$('#new_project').submit(function(){ $('#new_project').submit(function(){
var $path = $('#project_path'); var $path = $('#project_path');
...@@ -139,13 +135,13 @@ ...@@ -139,13 +135,13 @@
}); });
$('#project_path').keyup(function(){ $('#project_path').keyup(function(){
if($(this).val().length !=0) { if($(this).val().trim().length !== 0) {
$('.btn_import_gitlab_project').attr('disabled', false); $('.btn_import_gitlab_project').attr('disabled', false);
$('.import_gitlab_project').attr('title',''); $importBtnWrapper.attr('title','');
$(".flash-container").html("") $importBtnWrapper.removeClass('has-tooltip');
} else { } else {
$('.btn_import_gitlab_project').attr('disabled',true); $('.btn_import_gitlab_project').attr('disabled',true);
$('.import_gitlab_project').attr('title', 'Project path and name required.'); $importBtnWrapper.addClass('has-tooltip');
} }
}); });
......
- commit_message = @page.persisted? ? "Update #{@page.title}" : "Create #{@page.title}"
= form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form common-note-form prepend-top-default js-quick-submit' } do |f| = form_for [@project.namespace.becomes(Namespace), @project, @page], method: @page.persisted? ? :put : :post, html: { class: 'form-horizontal wiki-form common-note-form prepend-top-default js-quick-submit' } do |f|
= form_errors(@page) = form_errors(@page)
...@@ -28,7 +30,7 @@ ...@@ -28,7 +30,7 @@
.form-group .form-group
= f.label :commit_message, class: 'control-label' = f.label :commit_message, class: 'control-label'
.col-sm-10= f.text_field :message, class: 'form-control', rows: 18 .col-sm-10= f.text_field :message, class: 'form-control', rows: 18, value: commit_message
.form-actions .form-actions
- if @page && @page.persisted? - if @page && @page.persisted?
......
...@@ -6,4 +6,4 @@ ...@@ -6,4 +6,4 @@
= paginate @merge_requests, theme: "gitlab" = paginate @merge_requests, theme: "gitlab"
- else - else
.nothing-here-block No merge requests to show = render 'shared/empty_states/merge_requests'
- button_path = local_assigns.fetch(:button_path, false)
- project_select_button = local_assigns.fetch(:project_select_button, false)
- has_button = button_path || project_select_button
.row.empty-state.merge-requests
.col-xs-12{ class: "#{'col-sm-6 pull-right' if has_button}" }
.svg-content
= render 'shared/empty_states/icons/merge_requests.svg'
.col-xs-12{ class: "#{'col-sm-6' if has_button}" }
.text-content
- if has_button
%h4
Merge requests are a place to propose changes you've made to a project and discuss those changes with others.
%p
Interested parties can even contribute by pushing commits if they want to.
- if project_select_button
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: 'New merge request'
- else
= link_to 'New merge request', button_path, class: 'btn btn-new', title: 'New merge request', id: 'new_merge_request_link'
- else
%h4.text-center
There are no merge requests to show.
<svg xmlns="http://www.w3.org/2000/svg" viewBox="755 221 385 225" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="278" height="179" rx="10"/><mask id="d" width="278" height="179" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><path id="b" d="M13.6 49H57c5.5 0 10-4.5 10-10V10c0-5.5-4.5-10-10-10H10C4.5 0 0 4.5 0 10v42c0 5.5 3.2 7 7.2 3l6.4-6z"/><mask id="e" width="67" height="57.2" x="0" y="0" fill="#fff"><use xlink:href="#b"/></mask><path id="c" d="M13.6 49H57c5.5 0 10-4.5 10-10V10c0-5.5-4.5-10-10-10H10C4.5 0 0 4.5 0 10v42c0 5.5 3.2 7 7.2 3l6.4-6z"/><mask id="f" width="67" height="57.2" x="0" y="0" fill="#fff"><use xlink:href="#c"/></mask></defs><g fill="none" fill-rule="evenodd"><g fill="#F9F9F9" transform="translate(752 227)"><rect width="120" height="22" x="30" rx="11"/><rect width="132" height="22" y="44" rx="11"/><rect width="190" height="22" x="208" y="66" rx="11"/><rect width="158" height="22" x="129" y="197" rx="11"/><rect width="158" height="22" x="66" y="154" rx="11"/><rect width="350" height="22" x="31" y="110" rx="11"/><path d="M153 22H21h21.5c6 0 11 5 11 11s-5 11-11 11H21h132-36.5c-6 0-11-5-11-11s5-11 11-11H153zm252 66H288h36.5c6 0 11 5 11 11s-5 11-11 11H288h117-36.5c-6 0-11-5-11-11s5-11 11-11H405zm-244 44H44h36.5c6 0 11 5 11 11s-5 11-11 11H44h117-36.5c-6 0-11-5-11-11s5-11 11-11H161zm75 44H119h21.5c6 0 11 5 11 11s-5 11-11 11H119h117-51.5c-6 0-11-5-11-11s5-11 11-11H236z"/></g><g transform="translate(812 240)"><use fill="#FFF" stroke="#EEE" stroke-width="8" mask="url(#d)" xlink:href="#a"/><path fill="#EEE" d="M4 29h271v4H4z"/><g transform="translate(34 60)"><rect width="6" height="2" y="1" fill="#B5A7DD" rx="1"/><rect width="15" height="4" x="15" fill="#EEE" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#EEE" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="48" fill="#FC6D26" opacity=".5" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#EEE" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#FC6D26" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#B5A7DD" rx="1"/><rect width="6" height="2" y="23" fill="#B5A7DD" rx="1"/></g><g transform="translate(34 93)"><rect width="6" height="2" y="1" fill="#B5A7DD" rx="1"/><rect width="15" height="4" x="15" fill="#FC6D26" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#FC6D26" opacity=".5" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#EEE" rx="2"/><rect width="20" height="4" x="48" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#FC6D26" opacity=".5" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#B5A7DD" rx="1"/><rect width="6" height="2" y="23" fill="#B5A7DD" rx="1"/></g><g transform="translate(34 126)"><rect width="6" height="2" y="1" fill="#B5A7DD" rx="1"/><rect width="15" height="4" x="15" fill="#EEE" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#EEE" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#EEE" rx="2"/><rect width="20" height="4" x="48" fill="#FC6D26" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#EEE" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#FC6D26" opacity=".5" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#B5A7DD" rx="1"/><rect width="6" height="2" y="23" fill="#B5A7DD" rx="1"/></g><g transform="translate(157 59)"><rect width="6" height="2" y="1" fill="#FDE5D8" rx="1"/><rect width="15" height="4" x="15" fill="#EEE" rx="2"/><rect width="15" height="4" x="72" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="22" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="15" height="4" x="53" y="11" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="48" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="20" height="4" x="15" y="22" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="29" y="11" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" fill="#6B4FBB" rx="2"/><rect width="10" height="4" x="15" y="11" fill="#EEE" rx="2"/><rect width="6" height="2" y="12" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="23" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="34" fill="#FDE5D8" rx="1"/><rect width="15" height="4" x="15" y="33" fill="#EEE" rx="2"/><rect width="15" height="4" x="58" y="22" fill="#EEE" rx="2"/><rect width="15" height="4" x="39" y="55" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="15" height="4" x="29" y="44" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="48" y="33" fill="#6B4FBB" rx="2"/><rect width="20" height="4" x="15" y="55" fill="#EEE" rx="2"/><rect width="10" height="4" x="34" y="33" fill="#EEE" rx="2"/><rect width="10" height="4" x="15" y="44" fill="#EEE" rx="2"/><rect width="10" height="4" x="48" y="44" fill="#EEE" rx="2"/><rect width="10" height="4" x="62" y="44" fill="#EEE" rx="2"/><rect width="10" height="4" x="77" y="22" fill="#EEE" rx="2"/><rect width="6" height="2" y="45" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="56" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="67" fill="#FDE5D8" rx="1"/><rect width="15" height="4" x="15" y="66" fill="#6B4FBB" rx="2"/><rect width="15" height="4" x="39" y="88" fill="#EEE" rx="2"/><rect width="15" height="4" x="53" y="77" fill="#6B4FBB" opacity=".5" rx="2"/><rect width="20" height="4" x="15" y="88" fill="#EEE" rx="2"/><rect width="20" height="4" x="29" y="77" fill="#6B4FBB" rx="2"/><rect width="10" height="4" x="34" y="66" fill="#EEE" rx="2"/><rect width="10" height="4" x="72" y="77" fill="#EEE" rx="2"/><rect width="10" height="4" x="15" y="77" fill="#EEE" rx="2"/><rect width="6" height="2" y="78" fill="#FDE5D8" rx="1"/><rect width="6" height="2" y="89" fill="#FDE5D8" rx="1"/></g></g><g transform="translate(1057 221)"><use fill="#FFF" stroke="#FDE5D8" stroke-width="8" mask="url(#e)" xlink:href="#b"/><rect width="29" height="3" x="14" y="14" fill="#FDB692" rx="1.5"/><rect width="39" height="3" x="14" y="23" fill="#FDB692" rx="1.5"/><rect width="29" height="3" x="14" y="32" fill="#FDB692" rx="1.5"/></g><g transform="translate(1046 285)"><circle cx="16" cy="15" r="15" fill="#FFF7F4" stroke="#FC6D26" stroke-width="3"/><path stroke="#FC6D26" stroke-width="2" d="M0 14h1c5 0 9.2-2.7 11.4-6.7M14 1V0"/><path stroke="#FC6D26" stroke-width="2" d="M7.8 3c3 4.3 7.8 7 13.2 7 3.3 0 6.3-1 9-2.7"/><circle cx="10.5" cy="17.5" r="1.5" fill="#FC6D26"/><circle cx="21.5" cy="17.5" r="1.5" fill="#FC6D26"/></g><g transform="translate(825 370)"><circle cx="15" cy="16" r="15" fill="#F4F1FA" stroke="#6B4FBB" stroke-width="3"/><path fill="#6B4FBB" d="M25 7h2.7C25 2.8 20.4 0 15 0 9.6 0 5 2.8 2.3 7H5l2.5-3L10 7l2.5-3L15 7l2.5-3L20 7l2.5-3L25 7z"/><circle cx="9.5" cy="17.5" r="1.5" fill="#6B4FBB"/><circle cx="20.5" cy="17.5" r="1.5" fill="#6B4FBB"/></g><g transform="matrix(-1 0 0 1 840 306)"><use fill="#FFF" stroke="#E2DCF2" stroke-width="8" mask="url(#f)" xlink:href="#c"/><rect width="29" height="3" x="24" y="14" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="19" height="3" x="34" y="23" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="19" height="3" x="34" y="32" fill="#6B4FBB" opacity=".5" rx="1.5"/></g></g></svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
<title>path-1</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="_activity" fill="#7E7D7D">
<g id="Page-1">
<g id="path-1">
<path d="M5,0 C4.448,0 4,0.448 4,1 L4,3 L1,3 C0.448,3 0,3.448 0,4 L0,9 C0,9.552 0.448,10 1,10 L5,10 L5,8 L11,8 L11,10 L15,10 C15.552,10 16,9.552 16,9 L16,4 C16,3.448 15.552,3 15,3 L12,3 L12,1 C12,0.448 11.552,0 11,0 L5,0 L5,0 L5,0 L5,0 Z M6,2.5 C6,2.224 6.224,2 6.5,2 L9.5,2 C9.776,2 10,2.224 10,2.5 C10,2.776 9.776,3 9.5,3 L6.5,3 C6.224,3 6,2.776 6,2.5 L6,2.5 L6,2.5 L6,2.5 Z M6,11 L10.001,11 L10.001,9 L6,9 L6,11 L6,11 L6,11 L6,11 Z M11,11 L11,12 L5,12 L5,11 L1,11 C0.448,11 0,11.448 0,12 L0,15 C0,15.552 0.448,16 1,16 L15,16 C15.552,16 16,15.552 16,15 L16,12 C16,11.448 15.552,11 15,11 L11,11 L11,11 L11,11 L11,11 Z"></path>
</g>
</g>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
<title>Pasted Image 240</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<path d="M3,8 C3,5.951 4.236,4.194 6,3.422 L6,0 L1,0 C0.448,0 0,0.448 0,1 L0,15 C0,15.552 0.448,16 1,16 L6,16 L6,12.578 C4.236,11.806 3,10.049 3,8 M7,12.899 L7,16 L9,16 L9,12.899 C8.677,12.965 8.343,13 8,13 C7.657,13 7.323,12.965 7,12.899 M15,0 L10,0 L10,3.422 C11.764,4.194 13,5.951 13,8 C13,10.049 11.764,11.806 10,12.578 L10,16 L15,16 C15.552,16 16,15.552 16,15 L16,1 C16,0.448 15.552,0 15,0 M10,8 C10,9.105 9.105,10 8,10 C6.895,10 6,9.105 6,8 C6,6.895 6.895,6 8,6 C9.105,6 10,6.895 10,8 M4,8 C4,10.209 5.791,12 8,12 C10.209,12 12,10.209 12,8 C12,5.791 10.209,4 8,4 C5.791,4 4,5.791 4,8 M9,3.101 L9,0 L7,0 L7,3.101 C7.323,3.035 7.657,3 8,3 C8.343,3 8.677,3.035 9,3.101" id="Pasted-Image-240" fill="#7E7D7D"></path>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
<title>Group</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group">
<path d="M8,0 C3.581,0 0,3.581 0,8 C0,12.419 3.581,16 8,16 C12.419,16 16,12.419 16,8 C16,3.581 12.419,0 8,0 M8,2 C11.308,2 14,4.692 14,8 C14,11.308 11.308,14 8,14 C4.692,14 2,11.308 2,8 C2,4.692 4.692,2 8,2" id="Fill-1" fill="#7E7C7C"></path>
<polygon id="Stroke-6" fill="#7E7C7C" points="2.0197351 9.86809696 6.4567351 6.52409696 5.79233671 6.46815759 9.53233671 10.4271576 9.87070552 10.78534 10.2338016 10.4522494 15.0258016 6.05624938 14.3497984 5.31935062 9.55779844 9.71535062 10.2592633 9.74044241 6.51926329 5.78144241 6.21208651 5.45627854 5.8548649 5.72550304 1.4178649 9.06950304"></polygon>
<path d="M7.0313,6.3928 C7.0313,6.9448 6.5833,7.3928 6.0313,7.3928 C5.4793,7.3928 5.0313,6.9448 5.0313,6.3928 C5.0313,5.8408 5.4793,5.3928 6.0313,5.3928 C6.5833,5.3928 7.0313,5.8408 7.0313,6.3928" id="Fill-8" fill="#FEFEFE"></path>
<path d="M6.5313,6.3928 C6.5313,6.66865763 6.30715763,6.8928 6.0313,6.8928 C5.75544237,6.8928 5.5313,6.66865763 5.5313,6.3928 C5.5313,6.11694237 5.75544237,5.8928 6.0313,5.8928 C6.30715763,5.8928 6.5313,6.11694237 6.5313,6.3928 L6.5313,6.3928 Z M7.5313,6.3928 C7.5313,5.56465763 6.85944237,4.8928 6.0313,4.8928 C5.20315763,4.8928 4.5313,5.56465763 4.5313,6.3928 C4.5313,7.22094237 5.20315763,7.8928 6.0313,7.8928 C6.85944237,7.8928 7.5313,7.22094237 7.5313,6.3928 L7.5313,6.3928 Z" id="Stroke-10" fill="#7E7C7C"></path>
<path d="M10.8854,9.8715 C10.8854,10.4235 10.4374,10.8715 9.8854,10.8715 C9.3334,10.8715 8.8854,10.4235 8.8854,9.8715 C8.8854,9.3195 9.3334,8.8715 9.8854,8.8715 C10.4374,8.8715 10.8854,9.3195 10.8854,9.8715" id="Fill-12" fill="#FEFEFE"></path>
<path d="M10.3854,9.8715 C10.3854,10.1473576 10.1612576,10.3715 9.8854,10.3715 C9.60954237,10.3715 9.3854,10.1473576 9.3854,9.8715 C9.3854,9.59564237 9.60954237,9.3715 9.8854,9.3715 C10.1612576,9.3715 10.3854,9.59564237 10.3854,9.8715 L10.3854,9.8715 Z M11.3854,9.8715 C11.3854,9.04335763 10.7135424,8.3715 9.8854,8.3715 C9.05725763,8.3715 8.3854,9.04335763 8.3854,9.8715 C8.3854,10.6996424 9.05725763,11.3715 9.8854,11.3715 C10.7135424,11.3715 11.3854,10.6996424 11.3854,9.8715 L11.3854,9.8715 Z" id="Stroke-14" fill="#7E7C7C"></path>
</g>
</g>
</svg>
\ No newline at end of file
<svg width="14px" height="10px" viewBox="322 21 14 10" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<path d="M330.078605,22.8166945 L335.259532,29.6235062 C335.615145,30.0907182 335.412062,30.4694683 334.822641,30.4694683 L331.657805,30.4694683 L324.04678,30.4694683 C323.449879,30.4694683 323.260751,30.0822112 323.609889,29.6235062 L328.790816,22.8166945 C329.146429,22.3494825 329.729467,22.3579895 330.078605,22.8166945 Z" id="delta" stroke="#5C5C5C" stroke-width="1" fill="none"></path>
</svg>
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.8.3 (29802) - http://www.bohemiancoding.com/sketch -->
<title>Pasted Image 237</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Pasted-Image-237">
<path d="M15.1111,16 C15.6021,16 16.0001,15.602 16.0001,15.111 L16.0001,4.444 C15.5341,3.983 12.0671,0.378 11.5551,0 L0.8891,0 C0.3981,0 0.0001,0.398 0.0001,0.889 L0.0001,15.111 C0.0001,15.602 0.3981,16 0.8891,16 L15.1111,16 M14.0001,14.111 L1.8891,14.111 L1.8891,2 L10.8131,2 C11.4451,2.42 13.5811,4.555 14.0001,5.187 L14.0001,14.111" id="Fill-1" fill="#7E7D7D"></path>
<path d="M0.889,0 C0.398,0 0,0.398 0,0.889 L0,15.111 C0,15.602 0.398,16 0.889,16 L15.111,16 C15.602,16 16,15.602 16,15.111 L16,4.445 C15.534,3.983 12.068,0.377 11.555,0 L0.889,0 L0.889,0 Z M1.889,2 L10.813,2 C11.446,2.42 13.581,4.554 14,5.187 L14,14.111 L1.889,14.111 L1.889,2 L1.889,2 Z" id="Clip-4"></path>
<polygon id="Fill-6" fill="#7E7D7D" points="9 7 11 7 11 2 9 2"></polygon>
<polygon id="Clip-9" points="9 7 11 7 11 2.001 9 2.001"></polygon>
<polygon id="Fill-11" fill="#7E7D7D" points="10 7 15.444 7 15.444 5 10 5"></polygon>
<polygon id="Clip-14" points="10 7 15.444 7 15.444 5 10 5"></polygon>
</g>
</g>
</svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 11" class="icon-play"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 11" class="icon-play"><path fill-rule="evenodd" d="m9.283 6.47l-7.564 4.254c-.949.534-1.719.266-1.719-.576v-9.292c0-.852.756-1.117 1.719-.576l7.564 4.254c.949.534.963 1.392 0 1.934"/></svg>
<path fill-rule="evenodd" d="m9.283 6.47l-7.564 4.254c-.949.534-1.719.266-1.719-.576v-9.292c0-.852.756-1.117 1.719-.576l7.564 4.254c.949.534.963 1.392 0 1.934"/>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="22px" height="16px" viewBox="0 0 22 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
<title>Group</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" fill="#7E7C7C">
<path d="M6.4357,11.8588 C7.1487,11.2798 7.8797,10.7808 8.5357,10.3708 C8.5837,10.3008 8.6187,10.2338 8.6187,10.1768 L8.6187,8.8088 C8.9197,8.5218 9.0927,8.1248 9.0927,7.7028 L9.0927,5.3748 C9.0927,3.9478 7.9187,2.7858 6.4757,2.7858 L5.9687,2.7858 C4.5247,2.7858 3.3507,3.9478 3.3507,5.3748 L3.3507,7.7028 C3.3507,8.1248 3.5247,8.5218 3.8247,8.8088 L3.8247,10.5838 C3.2537,10.8738 1.8797,11.6198 0.5967,12.6618 C0.2177,12.9698 -0.0003,13.4258 -0.0003,13.9138 L-0.0003,15.5088 C-0.0003,15.5438 0.0857,15.7668 0.3467,15.7778 C1.3257,15.8198 3.8417,15.8328 5.9617,15.9038 C5.8337,15.8148 5.7447,15.6748 5.7447,15.5088 L5.7447,13.5498 C5.7447,12.9848 5.9967,12.2158 6.4357,11.8588" id="Fill-1"></path>
<path d="M21.3092,12.1 C19.6932,10.787 17.9592,9.86 17.3042,9.53 L17.3042,7.235 C17.6722,6.9 17.8862,6.428 17.8862,5.925 L17.8862,3.066 C17.8862,1.376 16.4952,0 14.7852,0 L14.1632,0 C12.4532,0 11.0622,1.376 11.0622,3.066 L11.0622,5.925 C11.0622,6.428 11.2752,6.9 11.6442,7.235 L11.6442,9.53 C10.9892,9.86 9.2542,10.787 7.6392,12.1 C7.2002,12.457 6.9482,12.985 6.9482,13.55 L6.9482,15.509 C6.9482,15.78 7.1702,16 7.4442,16 L14.1172,16 L14.1172,11.704 C12.6812,11.595 11.5652,10.853 11.5652,9.945 C11.5652,9.804 11.5982,9.669 11.6482,9.538 C11.9502,10.326 13.0982,10.913 14.4762,10.913 C15.8532,10.913 17.0012,10.326 17.3032,9.538 C17.3532,9.669 17.3862,9.804 17.3862,9.945 C17.3862,10.793 16.4152,11.5 15.1172,11.679 L15.1172,16 L21.5032,16 C21.7772,16 22.0002,15.78 22.0002,15.509 L22.0002,13.55 C22.0002,12.985 21.7482,12.457 21.3092,12.1" id="Fill-4"></path>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="17px" viewBox="0 0 16 17" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
<title>Group</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" fill="#7E7C7C">
<path d="M15.1111,1 L0.8891,1 C0.3981,1 0.0001,1.446 0.0001,1.996 L0.0001,15.945 C0.0001,16.495 0.3981,16.941 0.8891,16.941 L15.1111,16.941 C15.6021,16.941 16.0001,16.495 16.0001,15.945 L16.0001,1.996 C16.0001,1.446 15.6021,1 15.1111,1 L15.1111,1 L15.1111,1 Z M14.0001,6.0002 L14.0001,14.949 L2.0001,14.949 L2.0001,6.0002 L14.0001,6.0002 Z M14.0001,4.0002 L14.0001,2.993 L2.0001,2.993 L2.0001,4.0002 L14.0001,4.0002 Z" id="Combined-Shape"></path>
<polygon id="Fill-11" points="3 2.0002 5 2.0002 5 0.0002 3 0.0002"></polygon>
<polygon id="Fill-16" points="11 2.0002 13 2.0002 13 0.0002 11 0.0002"></polygon>
<path d="M5.37709616,11.5511984 L6.92309616,12.7821984 C7.35112915,13.123019 7.97359761,13.0565604 8.32002627,12.6330535 L10.7740263,9.63305349 C11.1237073,9.20557058 11.0606364,8.57555475 10.6331535,8.22587373 C10.2056706,7.87619272 9.57565475,7.93926361 9.22597373,8.36674651 L6.77197373,11.3667465 L8.16890384,11.2176016 L6.62290384,9.98660159 C6.19085236,9.6425813 5.56172188,9.71394467 5.21770159,10.1459962 C4.8736813,10.5780476 4.94504467,11.2071781 5.37709616,11.5511984 L5.37709616,11.5511984 Z" id="Stroke-21"></path>
</g>
</g>
</svg>
\ No newline at end of file
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="16px" height="16px" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.7.2 (28276) - http://www.bohemiancoding.com/sketch -->
<title>Group</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Group" fill="#7E7C7C">
<path d="M15.1111,0 L0.8891,0 C0.3981,0 0.0001,0.446 0.0001,0.996 L0.0001,14.945 C0.0001,15.495 0.3981,15.941 0.8891,15.941 L15.1111,15.941 C15.6021,15.941 16.0001,15.495 16.0001,14.945 L16.0001,0.996 C16.0001,0.446 15.6021,0 15.1111,0 L15.1111,0 L15.1111,0 Z M2.0001,13.949 L14.0001,13.949 L14.0001,1.993 L2.0001,1.993 L2.0001,13.949 Z M2,5.0002 L14,5.0002 L14,3.0002 L2,3.0002 L2,5.0002 Z" id="Combined-Shape"></path>
<path d="M8.547,12.0002 L12,12.0002 L12,10.0002 L8.547,10.0002 L8.547,12.0002 Z M5.2029,12 L3.9999,10.867 L5.2029,9.501 L3.9999,8.181 L5.2029,7 L7.4529,9.499 L5.2029,12 Z" id="Combined-Shape"></path>
</g>
</g>
</svg>
\ No newline at end of file
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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