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 ...
|
@ -8,6 +8,7 @@ cache:
|
|||
variables:
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: "1"
|
||||
RAILS_ENV: "test"
|
||||
NODE_ENV: "test"
|
||||
SIMPLECOV: "true"
|
||||
SETUP_DB: "true"
|
||||
USE_BUNDLE_INSTALL: "true"
|
||||
|
@ -129,9 +130,7 @@ setup-test-env:
|
|||
stage: prepare
|
||||
script:
|
||||
- node --version
|
||||
- yarn --version
|
||||
- yarn install --pure-lockfile
|
||||
- yarn check # ensure that yarn.lock matches package.json
|
||||
- bundle exec rake gitlab:assets:compile
|
||||
- bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
|
||||
artifacts:
|
||||
|
@ -296,8 +295,6 @@ docs:check:apilint:
|
|||
image: "phusion/baseimage"
|
||||
stage: test
|
||||
<<: *dedicated-runner
|
||||
variables:
|
||||
GIT_DEPTH: "3"
|
||||
cache: {}
|
||||
dependencies: []
|
||||
before_script: []
|
||||
|
@ -308,8 +305,6 @@ docs:check:links:
|
|||
image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
|
||||
stage: test
|
||||
<<: *dedicated-runner
|
||||
variables:
|
||||
GIT_DEPTH: "3"
|
||||
cache: {}
|
||||
dependencies: []
|
||||
before_script: []
|
||||
|
|
41
CHANGELOG.md
|
@ -2,6 +2,31 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
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)
|
||||
|
||||
- Correctly update paths when changing a child group.
|
||||
|
@ -303,6 +328,14 @@ entry.
|
|||
- Change development tanuki favicon colors to match logo color order.
|
||||
- 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)
|
||||
|
||||
- Only show public emails in atom feeds.
|
||||
|
@ -516,6 +549,14 @@ entry.
|
|||
- Remove deprecated GitlabCiService.
|
||||
- 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)
|
||||
|
||||
- Only show public emails in atom feeds.
|
||||
|
|
|
@ -1 +1 @@
|
|||
0.4.0
|
||||
0.5.0
|
||||
|
|
|
@ -1 +1 @@
|
|||
1.4.2
|
||||
1.4.3
|
||||
|
|
2
Gemfile
|
@ -223,7 +223,7 @@ gem 'oj', '~> 2.17.4'
|
|||
gem 'chronic', '~> 0.10.2'
|
||||
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 'sass-rails', '~> 5.0.6'
|
||||
|
|
|
@ -823,8 +823,8 @@ GEM
|
|||
addressable (>= 2.3.6)
|
||||
crack (>= 0.3.2)
|
||||
hashdiff
|
||||
webpack-rails (0.9.9)
|
||||
rails (>= 3.2.0)
|
||||
webpack-rails (0.9.10)
|
||||
railties (>= 3.2.0)
|
||||
websocket-driver (0.6.3)
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.2)
|
||||
|
@ -1026,7 +1026,7 @@ DEPENDENCIES
|
|||
virtus (~> 1.0.1)
|
||||
vmstat (~> 2.3.0)
|
||||
webmock (~> 1.24.0)
|
||||
webpack-rails (~> 0.9.9)
|
||||
webpack-rails (~> 0.9.10)
|
||||
wikicloth (= 0.8.1)
|
||||
|
||||
BUNDLED WITH
|
||||
|
|
|
@ -476,10 +476,10 @@ AwardsHandler.prototype.setupSearch = function setupSearch() {
|
|||
this.registerEventListener('on', $('input.emoji-search'), 'input', (e) => {
|
||||
const term = $(e.target).val().trim();
|
||||
// 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) {
|
||||
// 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 ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(foundEmojis);
|
||||
$('.emoji-menu-content ul, .emoji-menu-content h5').hide();
|
||||
|
|
62
app/assets/javascripts/blob/pdf/index.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
/* 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>
|
||||
`,
|
||||
});
|
||||
};
|
3
app/assets/javascripts/blob/pdf_viewer.js
Normal file
|
@ -0,0 +1,3 @@
|
|||
import renderPDF from './pdf';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', renderPDF);
|
73
app/assets/javascripts/blob/sketch/index.js
Normal file
|
@ -0,0 +1,73 @@
|
|||
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();
|
||||
}
|
||||
}
|
||||
}
|
8
app/assets/javascripts/blob/sketch_viewer.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
/* eslint-disable no-new */
|
||||
import SketchLoader from './sketch';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const el = document.getElementById('js-sketch-viewer');
|
||||
|
||||
new SketchLoader(el);
|
||||
});
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint-disable comma-dangle, space-before-function-paren, one-var */
|
||||
/* global Sortable */
|
||||
|
||||
import Vue from 'vue';
|
||||
import boardList from './board_list';
|
||||
import boardBlankState from './board_blank_state';
|
||||
|
||||
require('./board_delete');
|
||||
|
@ -16,7 +16,7 @@ require('./board_list');
|
|||
gl.issueBoards.Board = Vue.extend({
|
||||
template: '#js-board-template',
|
||||
components: {
|
||||
'board-list': gl.issueBoards.BoardList,
|
||||
boardList,
|
||||
'board-delete': gl.issueBoards.BoardDelete,
|
||||
boardBlankState,
|
||||
},
|
||||
|
|
|
@ -1,131 +1,197 @@
|
|||
/* eslint-disable comma-dangle, space-before-function-paren, max-len */
|
||||
/* global Sortable */
|
||||
|
||||
import Vue from 'vue';
|
||||
import boardNewIssue from './board_new_issue';
|
||||
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 || {};
|
||||
export default {
|
||||
name: 'BoardList',
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
list: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
issues: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
issueLinkBase: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
rootPath: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
scrollOffset: 250,
|
||||
filters: Store.state.filters,
|
||||
showCount: false,
|
||||
showIssueForm: false,
|
||||
};
|
||||
},
|
||||
components: {
|
||||
boardCard,
|
||||
boardNewIssue,
|
||||
},
|
||||
methods: {
|
||||
listHeight() {
|
||||
return this.$refs.list.getBoundingClientRect().height;
|
||||
},
|
||||
scrollHeight() {
|
||||
return this.$refs.list.scrollHeight;
|
||||
},
|
||||
scrollTop() {
|
||||
return this.$refs.list.scrollTop + this.listHeight();
|
||||
},
|
||||
loadNextPage() {
|
||||
const getIssues = this.list.nextPage();
|
||||
|
||||
gl.issueBoards.BoardList = Vue.extend({
|
||||
template: '#js-board-list-template',
|
||||
components: {
|
||||
boardCard,
|
||||
boardNewIssue,
|
||||
},
|
||||
props: {
|
||||
disabled: Boolean,
|
||||
list: Object,
|
||||
issues: Array,
|
||||
loading: Boolean,
|
||||
issueLinkBase: String,
|
||||
rootPath: String,
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
scrollOffset: 250,
|
||||
filters: Store.state.filters,
|
||||
showCount: false,
|
||||
showIssueForm: false
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
filters: {
|
||||
handler () {
|
||||
if (getIssues) {
|
||||
this.list.loadingMore = true;
|
||||
getIssues.then(() => {
|
||||
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: {
|
||||
listHeight () {
|
||||
return this.$refs.list.getBoundingClientRect().height;
|
||||
toggleForm() {
|
||||
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;
|
||||
},
|
||||
scrollHeight () {
|
||||
return this.$refs.list.scrollHeight;
|
||||
},
|
||||
scrollTop () {
|
||||
return this.$refs.list.scrollTop + this.listHeight();
|
||||
},
|
||||
loadNextPage () {
|
||||
const getIssues = this.list.nextPage();
|
||||
|
||||
if (getIssues) {
|
||||
this.list.loadingMore = true;
|
||||
getIssues.then(() => {
|
||||
this.list.loadingMore = false;
|
||||
});
|
||||
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);
|
||||
}
|
||||
},
|
||||
toggleForm() {
|
||||
this.showIssueForm = !this.showIssueForm;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
gl.IssueBoardsApp.$on(`hide-issue-form-${this.list.id}`, this.toggleForm);
|
||||
},
|
||||
mounted () {
|
||||
const options = gl.issueBoards.getBoardSortableDefaultOptions({
|
||||
scroll: document.querySelectorAll('.boards-list')[0],
|
||||
group: 'issues',
|
||||
disabled: this.disabled,
|
||||
filter: '.board-list-count, .is-disabled',
|
||||
dataIdAttr: 'data-issue-id',
|
||||
onStart: (e) => {
|
||||
const card = this.$refs.issue[e.oldIndex];
|
||||
|
||||
card.showDetail = false;
|
||||
Store.moving.list = card.list;
|
||||
Store.moving.issue = Store.moving.list.findIssue(+e.item.dataset.issueId);
|
||||
|
||||
gl.issueBoards.onStart();
|
||||
},
|
||||
onAdd: (e) => {
|
||||
gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex);
|
||||
|
||||
this.$nextTick(() => {
|
||||
e.item.remove();
|
||||
});
|
||||
},
|
||||
onUpdate: (e) => {
|
||||
const sortedArray = this.sortable.toArray().filter(id => id !== '-1');
|
||||
gl.issueBoards.BoardsStore.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray);
|
||||
},
|
||||
onMove(e) {
|
||||
return !e.related.classList.contains('board-list-count');
|
||||
if (this.scrollHeight() > Math.ceil(this.listHeight())) {
|
||||
this.showCount = true;
|
||||
} else {
|
||||
this.showCount = false;
|
||||
}
|
||||
});
|
||||
|
||||
this.sortable = Sortable.create(this.$refs.list, options);
|
||||
|
||||
// Scroll event on list to load more
|
||||
this.$refs.list.onscroll = () => {
|
||||
if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
|
||||
this.loadNextPage();
|
||||
}
|
||||
};
|
||||
},
|
||||
beforeDestroy() {
|
||||
gl.IssueBoardsApp.$off(`hide-issue-form-${this.list.id}`, this.toggleForm);
|
||||
},
|
||||
});
|
||||
})();
|
||||
},
|
||||
created() {
|
||||
eventHub.$on(`hide-issue-form-${this.list.id}`, this.toggleForm);
|
||||
},
|
||||
mounted() {
|
||||
const options = gl.issueBoards.getBoardSortableDefaultOptions({
|
||||
scroll: document.querySelectorAll('.boards-list')[0],
|
||||
group: 'issues',
|
||||
disabled: this.disabled,
|
||||
filter: '.board-list-count, .is-disabled',
|
||||
dataIdAttr: 'data-issue-id',
|
||||
onStart: (e) => {
|
||||
const card = this.$refs.issue[e.oldIndex];
|
||||
|
||||
card.showDetail = false;
|
||||
Store.moving.list = card.list;
|
||||
Store.moving.issue = Store.moving.list.findIssue(+e.item.dataset.issueId);
|
||||
|
||||
gl.issueBoards.onStart();
|
||||
},
|
||||
onAdd: (e) => {
|
||||
gl.issueBoards.BoardsStore
|
||||
.moveIssueToList(Store.moving.list, this.list, Store.moving.issue, e.newIndex);
|
||||
|
||||
this.$nextTick(() => {
|
||||
e.item.remove();
|
||||
});
|
||||
},
|
||||
onUpdate: (e) => {
|
||||
const sortedArray = this.sortable.toArray().filter(id => id !== '-1');
|
||||
gl.issueBoards.BoardsStore
|
||||
.moveIssueInList(this.list, Store.moving.issue, e.oldIndex, e.newIndex, sortedArray);
|
||||
},
|
||||
onMove(e) {
|
||||
return !e.related.classList.contains('board-list-count');
|
||||
},
|
||||
});
|
||||
|
||||
this.sortable = Sortable.create(this.$refs.list, options);
|
||||
|
||||
// Scroll event on list to load more
|
||||
this.$refs.list.addEventListener('scroll', this.onScroll);
|
||||
},
|
||||
beforeDestroy() {
|
||||
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>
|
||||
`,
|
||||
};
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
/* global ListIssue */
|
||||
import eventHub from '../eventhub';
|
||||
|
||||
const Store = gl.issueBoards.BoardsStore;
|
||||
|
||||
export default {
|
||||
|
@ -49,7 +51,7 @@ export default {
|
|||
},
|
||||
cancel() {
|
||||
this.title = '';
|
||||
gl.IssueBoardsApp.$emit(`hide-issue-form-${this.list.id}`);
|
||||
eventHub.$emit(`hide-issue-form-${this.list.id}`);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
|
|
|
@ -24,6 +24,7 @@ export default Vue.component('environment-component', {
|
|||
state: store.state,
|
||||
visibility: 'available',
|
||||
isLoading: false,
|
||||
isLoadingFolderContent: false,
|
||||
cssContainerClass: environmentsData.cssClass,
|
||||
endpoint: environmentsData.environmentsDataEndpoint,
|
||||
canCreateDeployment: environmentsData.canCreateDeployment,
|
||||
|
@ -68,15 +69,21 @@ export default Vue.component('environment-component', {
|
|||
this.fetchEnvironments();
|
||||
|
||||
eventHub.$on('refreshEnvironments', this.fetchEnvironments);
|
||||
eventHub.$on('toggleFolder', this.toggleFolder);
|
||||
},
|
||||
|
||||
beforeDestroyed() {
|
||||
eventHub.$off('refreshEnvironments');
|
||||
eventHub.$off('toggleFolder');
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleRow(model) {
|
||||
return this.store.toggleFolder(model.name);
|
||||
toggleFolder(folder, folderUrl) {
|
||||
this.store.toggleFolder(folder);
|
||||
|
||||
if (!folder.isOpen) {
|
||||
this.fetchChildEnvironments(folder, folderUrl);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -117,6 +124,21 @@ export default Vue.component('environment-component', {
|
|||
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: `
|
||||
|
@ -179,7 +201,8 @@ export default Vue.component('environment-component', {
|
|||
:environments="state.environments"
|
||||
:can-create-deployment="canCreateDeploymentParsed"
|
||||
:can-read-environment="canReadEnvironmentParsed"
|
||||
:service="service"/>
|
||||
:service="service"
|
||||
:is-loading-folder-content="isLoadingFolderContent" />
|
||||
</div>
|
||||
|
||||
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
|
||||
|
|
|
@ -45,11 +45,20 @@ export default {
|
|||
new Flash('An error occured while making the request.');
|
||||
});
|
||||
},
|
||||
|
||||
isActionDisabled(action) {
|
||||
if (action.playable === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !action.playable;
|
||||
},
|
||||
},
|
||||
|
||||
template: `
|
||||
<div class="btn-group" role="group">
|
||||
<button
|
||||
type="button"
|
||||
class="dropdown btn btn-default dropdown-new js-dropdown-play-icon-container has-tooltip"
|
||||
data-container="body"
|
||||
data-toggle="dropdown"
|
||||
|
@ -58,15 +67,23 @@ export default {
|
|||
:disabled="isLoading">
|
||||
<span>
|
||||
<span v-html="playIconSvg"></span>
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
|
||||
<i
|
||||
class="fa fa-caret-down"
|
||||
aria-hidden="true"/>
|
||||
<i
|
||||
v-if="isLoading"
|
||||
class="fa fa-spinner fa-spin"
|
||||
aria-hidden="true"/>
|
||||
</span>
|
||||
|
||||
<ul class="dropdown-menu dropdown-menu-align-right">
|
||||
<li v-for="action in actions">
|
||||
<button
|
||||
type="button"
|
||||
class="js-manual-action-link no-btn btn"
|
||||
@click="onClickAction(action.play_path)"
|
||||
class="js-manual-action-link no-btn">
|
||||
:class="{ 'disabled': isActionDisabled(action) }"
|
||||
:disabled="isActionDisabled(action)">
|
||||
${playIconSvg}
|
||||
<span>
|
||||
{{action.name}}
|
||||
|
|
|
@ -7,6 +7,7 @@ import RollbackComponent from './environment_rollback';
|
|||
import TerminalButtonComponent from './environment_terminal_button';
|
||||
import MonitoringButtonComponent from './environment_monitoring';
|
||||
import CommitComponent from '../../vue_shared/components/commit';
|
||||
import eventHub from '../event_hub';
|
||||
|
||||
/**
|
||||
* Envrionment Item Component
|
||||
|
@ -141,6 +142,7 @@ export default {
|
|||
const parsedAction = {
|
||||
name: gl.text.humanize(action.name),
|
||||
play_path: action.play_path,
|
||||
playable: action.playable,
|
||||
};
|
||||
return parsedAction;
|
||||
});
|
||||
|
@ -410,7 +412,6 @@ export default {
|
|||
folderUrl() {
|
||||
return `${window.location.pathname}/folders/${this.model.folderName}`;
|
||||
},
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -428,15 +429,37 @@ export default {
|
|||
return true;
|
||||
},
|
||||
|
||||
methods: {
|
||||
onClickFolder() {
|
||||
eventHub.$emit('toggleFolder', this.model, this.folderUrl);
|
||||
},
|
||||
},
|
||||
|
||||
template: `
|
||||
<tr>
|
||||
<tr :class="{ 'js-child-row': model.isChildren }">
|
||||
<td>
|
||||
<a v-if="!model.isFolder"
|
||||
class="environment-name"
|
||||
:class="{ 'prepend-left-default': model.isChildren }"
|
||||
:href="environmentPath">
|
||||
{{model.name}}
|
||||
</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">
|
||||
<i class="fa fa-folder" aria-hidden="true"></i>
|
||||
</span>
|
||||
|
@ -448,7 +471,7 @@ export default {
|
|||
<span class="badge">
|
||||
{{model.size}}
|
||||
</span>
|
||||
</a>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="deployment-column">
|
||||
|
|
|
@ -31,6 +31,18 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
||||
isLoadingFolderContent: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
folderUrl(model) {
|
||||
return `${window.location.pathname}/folders/${model.folderName}`;
|
||||
},
|
||||
},
|
||||
|
||||
template: `
|
||||
|
@ -53,6 +65,31 @@ export default {
|
|||
:can-create-deployment="canCreateDeployment"
|
||||
:can-read-environment="canReadEnvironment"
|
||||
: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>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
|
@ -7,6 +7,7 @@ Vue.use(VueResource);
|
|||
export default class EnvironmentsService {
|
||||
constructor(endpoint) {
|
||||
this.environments = Vue.resource(endpoint);
|
||||
this.folderResults = 3;
|
||||
}
|
||||
|
||||
get(scope, page) {
|
||||
|
@ -16,4 +17,8 @@ export default class EnvironmentsService {
|
|||
postAction(endpoint) {
|
||||
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 {
|
|||
let filtered = {};
|
||||
|
||||
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) {
|
||||
|
@ -85,4 +90,67 @@ export default class EnvironmentsStore {
|
|||
this.state.stoppedCounter = 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() {
|
|||
page,
|
||||
per_page: GroupsSelect.PER_PAGE,
|
||||
all_available,
|
||||
skip_groups,
|
||||
};
|
||||
},
|
||||
results: function (data, page) {
|
||||
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 results = groups.filter(group => skip_groups.indexOf(group.id) === -1);
|
||||
|
||||
return {
|
||||
results,
|
||||
|
|
26
app/assets/javascripts/issue_show/index.js
Normal file
|
@ -0,0 +1,26 @@
|
|||
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()))();
|
78
app/assets/javascripts/issue_show/issue_title.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
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>
|
||||
`,
|
||||
};
|
10
app/assets/javascripts/issue_show/services/index.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
export default class Service {
|
||||
constructor(resource, endpoint) {
|
||||
this.resource = resource;
|
||||
this.endpoint = endpoint;
|
||||
}
|
||||
|
||||
getTitle() {
|
||||
return this.resource.get(this.endpoint);
|
||||
}
|
||||
}
|
34
app/assets/javascripts/lib/utils/number_utils.js
Normal file
|
@ -0,0 +1,34 @@
|
|||
/* 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';
|
|||
import './wikis';
|
||||
import './zen_mode';
|
||||
|
||||
// eslint-disable-next-line global-require
|
||||
if (process.env.NODE_ENV !== 'production') require('./test_utils/');
|
||||
|
||||
document.addEventListener('beforeunload', function () {
|
||||
// Unbind scroll events
|
||||
$(document).off('scroll');
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
/* eslint-disable no-new*/
|
||||
/* eslint-disable no-new */
|
||||
/* global Flash */
|
||||
|
||||
import d3 from 'd3';
|
||||
import statusCodes from '~/lib/utils/http_status';
|
||||
import '../lib/utils/common_utils';
|
||||
import { formatRelevantDigits } from '~/lib/utils/number_utils';
|
||||
import '../flash';
|
||||
|
||||
const prometheusGraphsContainer = '.prometheus-graph';
|
||||
|
@ -21,19 +21,19 @@ class PrometheusGraph {
|
|||
const parentContainerWidth = $(prometheusGraphsContainer).parent().width() +
|
||||
extraAddedWidthParent;
|
||||
this.originalWidth = parentContainerWidth;
|
||||
this.originalHeight = 400;
|
||||
this.originalHeight = 330;
|
||||
this.width = parentContainerWidth - this.margin.left - this.margin.right;
|
||||
this.height = 400 - this.margin.top - this.margin.bottom;
|
||||
this.height = this.originalHeight - this.margin.top - this.margin.bottom;
|
||||
this.backOffRequestCounter = 0;
|
||||
this.configureGraph();
|
||||
this.init();
|
||||
}
|
||||
|
||||
createGraph() {
|
||||
Object.keys(this.data).forEach((key) => {
|
||||
const value = this.data[key];
|
||||
if (value.length > 0) {
|
||||
this.plotValues(value, key);
|
||||
Object.keys(this.graphSpecificProperties).forEach((key) => {
|
||||
const value = this.graphSpecificProperties[key];
|
||||
if (value.data.length > 0) {
|
||||
this.plotValues(key);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -49,53 +49,56 @@ class PrometheusGraph {
|
|||
});
|
||||
}
|
||||
|
||||
plotValues(valuesToPlot, key) {
|
||||
plotValues(key) {
|
||||
const graphSpecifics = this.graphSpecificProperties[key];
|
||||
|
||||
const x = d3.time.scale()
|
||||
.range([0, this.width]);
|
||||
|
||||
const y = d3.scale.linear()
|
||||
.range([this.height, 0]);
|
||||
|
||||
graphSpecifics.xScale = x;
|
||||
graphSpecifics.yScale = y;
|
||||
|
||||
const prometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`;
|
||||
|
||||
const graphSpecifics = this.graphSpecificProperties[key];
|
||||
|
||||
const chart = d3.select(prometheusGraphContainer)
|
||||
.attr('width', this.width + this.margin.left + this.margin.right)
|
||||
.attr('height', this.height + this.margin.bottom + this.margin.top)
|
||||
.append('g')
|
||||
.attr('transform', `translate(${this.margin.left},${this.margin.top})`);
|
||||
.attr('width', this.width + this.margin.left + this.margin.right)
|
||||
.attr('height', this.height + this.margin.bottom + this.margin.top)
|
||||
.append('g')
|
||||
.attr('transform', `translate(${this.margin.left},${this.margin.top})`);
|
||||
|
||||
const axisLabelContainer = d3.select(prometheusGraphContainer)
|
||||
.attr('width', this.originalWidth + this.marginLabelContainer.left + this.marginLabelContainer.right)
|
||||
.attr('height', this.originalHeight + this.marginLabelContainer.bottom + this.marginLabelContainer.top)
|
||||
.attr('width', this.originalWidth)
|
||||
.attr('height', this.originalHeight)
|
||||
.append('g')
|
||||
.attr('transform', `translate(${this.marginLabelContainer.left},${this.marginLabelContainer.top})`);
|
||||
|
||||
x.domain(d3.extent(valuesToPlot, d => d.time));
|
||||
y.domain([0, d3.max(valuesToPlot.map(metricValue => metricValue.value))]);
|
||||
x.domain(d3.extent(graphSpecifics.data, d => d.time));
|
||||
y.domain([0, d3.max(graphSpecifics.data.map(metricValue => metricValue.value))]);
|
||||
|
||||
const xAxis = d3.svg.axis()
|
||||
.scale(x)
|
||||
.ticks(this.commonGraphProperties.axis_no_ticks)
|
||||
.orient('bottom');
|
||||
.scale(x)
|
||||
.ticks(this.commonGraphProperties.axis_no_ticks)
|
||||
.orient('bottom');
|
||||
|
||||
const yAxis = d3.svg.axis()
|
||||
.scale(y)
|
||||
.ticks(this.commonGraphProperties.axis_no_ticks)
|
||||
.tickSize(-this.width)
|
||||
.orient('left');
|
||||
.scale(y)
|
||||
.ticks(this.commonGraphProperties.axis_no_ticks)
|
||||
.tickSize(-this.width)
|
||||
.orient('left');
|
||||
|
||||
this.createAxisLabelContainers(axisLabelContainer, key);
|
||||
|
||||
chart.append('g')
|
||||
.attr('class', 'x-axis')
|
||||
.attr('transform', `translate(0,${this.height})`)
|
||||
.call(xAxis);
|
||||
.attr('class', 'x-axis')
|
||||
.attr('transform', `translate(0,${this.height})`)
|
||||
.call(xAxis);
|
||||
|
||||
chart.append('g')
|
||||
.attr('class', 'y-axis')
|
||||
.call(yAxis);
|
||||
.attr('class', 'y-axis')
|
||||
.call(yAxis);
|
||||
|
||||
const area = d3.svg.area()
|
||||
.x(d => x(d.time))
|
||||
|
@ -108,13 +111,13 @@ class PrometheusGraph {
|
|||
.y(d => y(d.value));
|
||||
|
||||
chart.append('path')
|
||||
.datum(valuesToPlot)
|
||||
.attr('d', area)
|
||||
.attr('class', 'metric-area')
|
||||
.attr('fill', graphSpecifics.area_fill_color);
|
||||
.datum(graphSpecifics.data)
|
||||
.attr('d', area)
|
||||
.attr('class', 'metric-area')
|
||||
.attr('fill', graphSpecifics.area_fill_color);
|
||||
|
||||
chart.append('path')
|
||||
.datum(valuesToPlot)
|
||||
.datum(graphSpecifics.data)
|
||||
.attr('class', 'metric-line')
|
||||
.attr('stroke', graphSpecifics.line_color)
|
||||
.attr('fill', 'none')
|
||||
|
@ -126,7 +129,7 @@ class PrometheusGraph {
|
|||
.attr('class', 'prometheus-graph-overlay')
|
||||
.attr('width', this.width)
|
||||
.attr('height', this.height)
|
||||
.on('mousemove', this.handleMouseOverGraph.bind(this, x, y, valuesToPlot, chart, prometheusGraphContainer, key));
|
||||
.on('mousemove', this.handleMouseOverGraph.bind(this, prometheusGraphContainer));
|
||||
}
|
||||
|
||||
// The legends from the metric
|
||||
|
@ -134,128 +137,150 @@ class PrometheusGraph {
|
|||
const graphSpecifics = this.graphSpecificProperties[key];
|
||||
|
||||
axisLabelContainer.append('line')
|
||||
.attr('class', 'label-x-axis-line')
|
||||
.attr('stroke', '#000000')
|
||||
.attr('stroke-width', '1')
|
||||
.attr({
|
||||
x1: 0,
|
||||
y1: this.originalHeight - this.marginLabelContainer.top,
|
||||
x2: this.originalWidth - this.margin.right,
|
||||
y2: this.originalHeight - this.marginLabelContainer.top,
|
||||
});
|
||||
.attr('class', 'label-x-axis-line')
|
||||
.attr('stroke', '#000000')
|
||||
.attr('stroke-width', '1')
|
||||
.attr({
|
||||
x1: 10,
|
||||
y1: this.originalHeight - this.margin.top,
|
||||
x2: (this.originalWidth - this.margin.right) + 10,
|
||||
y2: this.originalHeight - this.margin.top,
|
||||
});
|
||||
|
||||
axisLabelContainer.append('line')
|
||||
.attr('class', 'label-y-axis-line')
|
||||
.attr('stroke', '#000000')
|
||||
.attr('stroke-width', '1')
|
||||
.attr({
|
||||
x1: 0,
|
||||
y1: 0,
|
||||
x2: 0,
|
||||
y2: this.originalHeight - this.marginLabelContainer.top,
|
||||
});
|
||||
|
||||
axisLabelContainer.append('text')
|
||||
.attr('class', 'label-axis-text')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('transform', `translate(15, ${(this.originalHeight - this.marginLabelContainer.top) / 2}) rotate(-90)`)
|
||||
.text(graphSpecifics.graph_legend_title);
|
||||
.attr('class', 'label-y-axis-line')
|
||||
.attr('stroke', '#000000')
|
||||
.attr('stroke-width', '1')
|
||||
.attr({
|
||||
x1: 10,
|
||||
y1: 0,
|
||||
x2: 10,
|
||||
y2: this.originalHeight - this.margin.top,
|
||||
});
|
||||
|
||||
axisLabelContainer.append('rect')
|
||||
.attr('class', 'rect-axis-text')
|
||||
.attr('x', (this.originalWidth / 2) - this.margin.right)
|
||||
.attr('y', this.originalHeight - this.marginLabelContainer.top - 20)
|
||||
.attr('width', 30)
|
||||
.attr('height', 80);
|
||||
.attr('class', 'rect-axis-text')
|
||||
.attr('x', 0)
|
||||
.attr('y', 50)
|
||||
.attr('width', 30)
|
||||
.attr('height', 150);
|
||||
|
||||
axisLabelContainer.append('text')
|
||||
.attr('class', 'label-axis-text')
|
||||
.attr('x', (this.originalWidth / 2) - this.margin.right)
|
||||
.attr('y', this.originalHeight - this.marginLabelContainer.top)
|
||||
.attr('dy', '.35em')
|
||||
.text('Time');
|
||||
.attr('class', 'label-axis-text')
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('transform', `translate(15, ${(this.originalHeight - this.margin.top) / 2}) rotate(-90)`)
|
||||
.text(graphSpecifics.graph_legend_title);
|
||||
|
||||
axisLabelContainer.append('rect')
|
||||
.attr('class', 'rect-axis-text')
|
||||
.attr('x', (this.originalWidth / 2) - this.margin.right)
|
||||
.attr('y', this.originalHeight - 100)
|
||||
.attr('width', 30)
|
||||
.attr('height', 80);
|
||||
|
||||
axisLabelContainer.append('text')
|
||||
.attr('class', 'label-axis-text')
|
||||
.attr('x', (this.originalWidth / 2) - this.margin.right)
|
||||
.attr('y', this.originalHeight - this.margin.top)
|
||||
.attr('dy', '.35em')
|
||||
.text('Time');
|
||||
|
||||
// Legends
|
||||
|
||||
// Metric Usage
|
||||
axisLabelContainer.append('rect')
|
||||
.attr('x', this.originalWidth - 170)
|
||||
.attr('y', (this.originalHeight / 2) - 60)
|
||||
.style('fill', graphSpecifics.area_fill_color)
|
||||
.attr('width', 20)
|
||||
.attr('height', 35);
|
||||
.attr('x', this.originalWidth - 170)
|
||||
.attr('y', (this.originalHeight / 2) - 60)
|
||||
.style('fill', graphSpecifics.area_fill_color)
|
||||
.attr('width', 20)
|
||||
.attr('height', 35);
|
||||
|
||||
axisLabelContainer.append('text')
|
||||
.attr('class', 'label-axis-text')
|
||||
.attr('x', this.originalWidth - 140)
|
||||
.attr('y', (this.originalHeight / 2) - 50)
|
||||
.text('Average');
|
||||
.attr('class', 'text-metric-title')
|
||||
.attr('x', this.originalWidth - 140)
|
||||
.attr('y', (this.originalHeight / 2) - 50)
|
||||
.text('Average');
|
||||
|
||||
axisLabelContainer.append('text')
|
||||
.attr('class', 'text-metric-usage')
|
||||
.attr('x', this.originalWidth - 140)
|
||||
.attr('y', (this.originalHeight / 2) - 25);
|
||||
.attr('class', 'text-metric-usage')
|
||||
.attr('x', this.originalWidth - 140)
|
||||
.attr('y', (this.originalHeight / 2) - 25);
|
||||
}
|
||||
|
||||
handleMouseOverGraph(x, y, valuesToPlot, chart, prometheusGraphContainer, key) {
|
||||
handleMouseOverGraph(prometheusGraphContainer) {
|
||||
const rectOverlay = document.querySelector(`${prometheusGraphContainer} .prometheus-graph-overlay`);
|
||||
const timeValueFromOverlay = x.invert(d3.mouse(rectOverlay)[0]);
|
||||
const timeValueIndex = bisectDate(valuesToPlot, timeValueFromOverlay, 1);
|
||||
const d0 = valuesToPlot[timeValueIndex - 1];
|
||||
const d1 = valuesToPlot[timeValueIndex];
|
||||
const currentData = timeValueFromOverlay - d0.time > d1.time - timeValueFromOverlay ? d1 : d0;
|
||||
const maxValueMetric = y(d3.max(valuesToPlot.map(metricValue => metricValue.value)));
|
||||
const currentTimeCoordinate = x(currentData.time);
|
||||
const graphSpecifics = this.graphSpecificProperties[key];
|
||||
// Remove the current selectors
|
||||
d3.selectAll(`${prometheusGraphContainer} .selected-metric-line`).remove();
|
||||
d3.selectAll(`${prometheusGraphContainer} .circle-metric`).remove();
|
||||
d3.selectAll(`${prometheusGraphContainer} .rect-text-metric`).remove();
|
||||
d3.selectAll(`${prometheusGraphContainer} .text-metric`).remove();
|
||||
const currentXCoordinate = d3.mouse(rectOverlay)[0];
|
||||
|
||||
chart.append('line')
|
||||
.attr('class', 'selected-metric-line')
|
||||
.attr({
|
||||
x1: currentTimeCoordinate,
|
||||
y1: y(0),
|
||||
x2: currentTimeCoordinate,
|
||||
y2: maxValueMetric,
|
||||
Object.keys(this.graphSpecificProperties).forEach((key) => {
|
||||
const currentGraphProps = this.graphSpecificProperties[key];
|
||||
const timeValueOverlay = currentGraphProps.xScale.invert(currentXCoordinate);
|
||||
const overlayIndex = bisectDate(currentGraphProps.data, timeValueOverlay, 1);
|
||||
const d0 = currentGraphProps.data[overlayIndex - 1];
|
||||
const d1 = currentGraphProps.data[overlayIndex];
|
||||
const evalTime = timeValueOverlay - d0.time > d1.time - timeValueOverlay;
|
||||
const currentData = evalTime ? d1 : d0;
|
||||
const currentTimeCoordinate = currentGraphProps.xScale(currentData.time);
|
||||
const currentPrometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`;
|
||||
const maxValueFromData = d3.max(currentGraphProps.data.map(metricValue => metricValue.value));
|
||||
const maxMetricValue = currentGraphProps.yScale(maxValueFromData);
|
||||
|
||||
// Clear up all the pieces of the flag
|
||||
d3.selectAll(`${currentPrometheusGraphContainer} .selected-metric-line`).remove();
|
||||
d3.selectAll(`${currentPrometheusGraphContainer} .circle-metric`).remove();
|
||||
d3.selectAll(`${currentPrometheusGraphContainer} .rect-text-metric`).remove();
|
||||
d3.selectAll(`${currentPrometheusGraphContainer} .text-metric`).remove();
|
||||
|
||||
const currentChart = d3.select(currentPrometheusGraphContainer).select('g');
|
||||
currentChart.append('line')
|
||||
.attr('class', 'selected-metric-line')
|
||||
.attr({
|
||||
x1: currentTimeCoordinate,
|
||||
y1: currentGraphProps.yScale(0),
|
||||
x2: currentTimeCoordinate,
|
||||
y2: maxMetricValue,
|
||||
});
|
||||
|
||||
currentChart.append('circle')
|
||||
.attr('class', 'circle-metric')
|
||||
.attr('fill', currentGraphProps.line_color)
|
||||
.attr('cx', currentTimeCoordinate)
|
||||
.attr('cy', currentGraphProps.yScale(currentData.value))
|
||||
.attr('r', this.commonGraphProperties.circle_radius_metric);
|
||||
|
||||
// The little box with text
|
||||
const rectTextMetric = currentChart.append('g')
|
||||
.attr('class', 'rect-text-metric')
|
||||
.attr('translate', `(${currentTimeCoordinate}, ${currentGraphProps.yScale(currentData.value)})`);
|
||||
|
||||
rectTextMetric.append('rect')
|
||||
.attr('class', 'rect-metric')
|
||||
.attr('x', currentTimeCoordinate + 10)
|
||||
.attr('y', maxMetricValue)
|
||||
.attr('width', this.commonGraphProperties.rect_text_width)
|
||||
.attr('height', this.commonGraphProperties.rect_text_height);
|
||||
|
||||
rectTextMetric.append('text')
|
||||
.attr('class', 'text-metric')
|
||||
.attr('x', currentTimeCoordinate + 35)
|
||||
.attr('y', maxMetricValue + 35)
|
||||
.text(timeFormat(currentData.time));
|
||||
|
||||
rectTextMetric.append('text')
|
||||
.attr('class', 'text-metric-date')
|
||||
.attr('x', currentTimeCoordinate + 15)
|
||||
.attr('y', maxMetricValue + 15)
|
||||
.text(dayFormat(currentData.time));
|
||||
|
||||
let currentMetricValue = formatRelevantDigits(currentData.value);
|
||||
if (key === 'cpu_values') {
|
||||
currentMetricValue = `${currentMetricValue}%`;
|
||||
} else {
|
||||
currentMetricValue = `${currentMetricValue} MB`;
|
||||
}
|
||||
|
||||
d3.select(`${currentPrometheusGraphContainer} .text-metric-usage`)
|
||||
.text(currentMetricValue);
|
||||
});
|
||||
|
||||
chart.append('circle')
|
||||
.attr('class', 'circle-metric')
|
||||
.attr('fill', graphSpecifics.line_color)
|
||||
.attr('cx', currentTimeCoordinate)
|
||||
.attr('cy', y(currentData.value))
|
||||
.attr('r', this.commonGraphProperties.circle_radius_metric);
|
||||
|
||||
// The little box with text
|
||||
const rectTextMetric = chart.append('g')
|
||||
.attr('class', 'rect-text-metric')
|
||||
.attr('translate', `(${currentTimeCoordinate}, ${y(currentData.value)})`);
|
||||
|
||||
rectTextMetric.append('rect')
|
||||
.attr('class', 'rect-metric')
|
||||
.attr('x', currentTimeCoordinate + 10)
|
||||
.attr('y', maxValueMetric)
|
||||
.attr('width', this.commonGraphProperties.rect_text_width)
|
||||
.attr('height', this.commonGraphProperties.rect_text_height);
|
||||
|
||||
rectTextMetric.append('text')
|
||||
.attr('class', 'text-metric')
|
||||
.attr('x', currentTimeCoordinate + 35)
|
||||
.attr('y', maxValueMetric + 35)
|
||||
.text(timeFormat(currentData.time));
|
||||
|
||||
rectTextMetric.append('text')
|
||||
.attr('class', 'text-metric-date')
|
||||
.attr('x', currentTimeCoordinate + 15)
|
||||
.attr('y', maxValueMetric + 15)
|
||||
.text(dayFormat(currentData.time));
|
||||
|
||||
// Update the text
|
||||
d3.select(`${prometheusGraphContainer} .text-metric-usage`)
|
||||
.text(currentData.value.substring(0, 8));
|
||||
}
|
||||
|
||||
configureGraph() {
|
||||
|
@ -263,12 +288,18 @@ class PrometheusGraph {
|
|||
cpu_values: {
|
||||
area_fill_color: '#edf3fc',
|
||||
line_color: '#5b99f7',
|
||||
graph_legend_title: 'CPU utilization (%)',
|
||||
graph_legend_title: 'CPU Usage (Cores)',
|
||||
data: [],
|
||||
xScale: {},
|
||||
yScale: {},
|
||||
},
|
||||
memory_values: {
|
||||
area_fill_color: '#fca326',
|
||||
line_color: '#fc6d26',
|
||||
graph_legend_title: 'Memory usage (MB)',
|
||||
graph_legend_title: 'Memory Usage (MB)',
|
||||
data: [],
|
||||
xScale: {},
|
||||
yScale: {},
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -318,17 +349,17 @@ class PrometheusGraph {
|
|||
}
|
||||
|
||||
transformData(metricsResponse) {
|
||||
const metricTypes = {};
|
||||
Object.keys(metricsResponse.metrics).forEach((key) => {
|
||||
if (key === 'cpu_values' || key === 'memory_values') {
|
||||
const metricValues = (metricsResponse.metrics[key])[0];
|
||||
metricTypes[key] = metricValues.values.map(metric => ({
|
||||
time: new Date(metric[0] * 1000),
|
||||
value: metric[1],
|
||||
}));
|
||||
if (typeof metricValues !== 'undefined') {
|
||||
this.graphSpecificProperties[key].data = metricValues.values.map(metric => ({
|
||||
time: new Date(metric[0] * 1000),
|
||||
value: metric[1],
|
||||
}));
|
||||
}
|
||||
}
|
||||
});
|
||||
this.data = metricTypes;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
4
app/assets/javascripts/test_utils/index.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
import simulateDrag from './simulate_drag';
|
||||
|
||||
// Export to global space for rspec to use
|
||||
window.simulateDrag = simulateDrag;
|
|
@ -1,143 +1,137 @@
|
|||
/* 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 () {
|
||||
'use strict';
|
||||
function simulateEvent(el, type, options = {}) {
|
||||
let event;
|
||||
if (!el) return null;
|
||||
|
||||
function simulateEvent(el, type, options) {
|
||||
var event;
|
||||
if (!el) return;
|
||||
var ownerDocument = el.ownerDocument;
|
||||
if (/^mouse/.test(type)) {
|
||||
event = el.ownerDocument.createEvent('MouseEvents');
|
||||
event.initMouseEvent(type, true, true, el.ownerDocument.defaultView,
|
||||
options.button, options.screenX, options.screenY, options.clientX, options.clientY,
|
||||
options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
|
||||
} else {
|
||||
event = el.ownerDocument.createEvent('CustomEvent');
|
||||
|
||||
options = options || {};
|
||||
event.initCustomEvent(type, true, true, el.ownerDocument.defaultView,
|
||||
options.button, options.screenX, options.screenY, options.clientX, options.clientY,
|
||||
options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
|
||||
|
||||
if (/^mouse/.test(type)) {
|
||||
event = ownerDocument.createEvent('MouseEvents');
|
||||
event.initMouseEvent(type, true, true, ownerDocument.defaultView,
|
||||
options.button, options.screenX, options.screenY, options.clientX, options.clientY,
|
||||
options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
|
||||
} else {
|
||||
event = ownerDocument.createEvent('CustomEvent');
|
||||
event.dataTransfer = {
|
||||
data: {},
|
||||
|
||||
event.initCustomEvent(type, true, true, ownerDocument.defaultView,
|
||||
options.button, options.screenX, options.screenY, options.clientX, options.clientY,
|
||||
options.ctrlKey, options.altKey, options.shiftKey, options.metaKey, options.button, el);
|
||||
setData(key, val) {
|
||||
this.data[key] = val;
|
||||
},
|
||||
|
||||
event.dataTransfer = {
|
||||
data: {},
|
||||
|
||||
setData: function (type, val) {
|
||||
this.data[type] = val;
|
||||
},
|
||||
|
||||
getData: function (type) {
|
||||
return this.data[type];
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
if (el.dispatchEvent) {
|
||||
el.dispatchEvent(event);
|
||||
} else if (el.fireEvent) {
|
||||
el.fireEvent('on' + type, event);
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
function isLast(target) {
|
||||
var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
|
||||
var children = el.children;
|
||||
|
||||
return children.length - 1 === target.index;
|
||||
}
|
||||
|
||||
function getTarget(target) {
|
||||
var el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
|
||||
var children = el.children;
|
||||
|
||||
return (
|
||||
children[target.index] ||
|
||||
children[target.index === 'first' ? 0 : -1] ||
|
||||
children[target.index === 'last' ? children.length - 1 : -1] ||
|
||||
el
|
||||
);
|
||||
}
|
||||
|
||||
function getRect(el) {
|
||||
var rect = el.getBoundingClientRect();
|
||||
var width = rect.right - rect.left;
|
||||
var height = rect.bottom - rect.top + 10;
|
||||
|
||||
return {
|
||||
x: rect.left,
|
||||
y: rect.top,
|
||||
cx: rect.left + width / 2,
|
||||
cy: rect.top + height / 2,
|
||||
w: width,
|
||||
h: height,
|
||||
hw: width / 2,
|
||||
wh: height / 2
|
||||
getData(key) {
|
||||
return this.data[key];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function simulateDrag(options, callback) {
|
||||
options.to.el = options.to.el || options.from.el;
|
||||
|
||||
var fromEl = getTarget(options.from);
|
||||
var toEl = getTarget(options.to);
|
||||
var firstEl = getTarget({
|
||||
el: options.to.el,
|
||||
index: 'first'
|
||||
});
|
||||
var lastEl = getTarget({
|
||||
el: options.to.el,
|
||||
index: 'last'
|
||||
});
|
||||
var scrollable = options.scrollable;
|
||||
|
||||
var fromRect = getRect(fromEl);
|
||||
var toRect = getRect(toEl);
|
||||
var firstRect = getRect(firstEl);
|
||||
var lastRect = getRect(lastEl);
|
||||
|
||||
var startTime = new Date().getTime();
|
||||
var duration = options.duration || 1000;
|
||||
simulateEvent(fromEl, 'mousedown', { button: 0 });
|
||||
options.ontap && options.ontap();
|
||||
window.SIMULATE_DRAG_ACTIVE = 1;
|
||||
|
||||
if (options.to.index === 0) {
|
||||
toRect.cy = firstRect.y;
|
||||
} else if (isLast(options.to)) {
|
||||
toRect.cy = lastRect.y + lastRect.h + 50;
|
||||
}
|
||||
|
||||
var dragInterval = setInterval(function loop() {
|
||||
var progress = (new Date().getTime() - startTime) / duration;
|
||||
var x = (fromRect.cx + (toRect.cx - fromRect.cx) * progress) - scrollable.scrollLeft;
|
||||
var y = (fromRect.cy + (toRect.cy - fromRect.cy) * progress) - scrollable.scrollTop;
|
||||
var overEl = fromEl.ownerDocument.elementFromPoint(x, y);
|
||||
|
||||
simulateEvent(overEl, 'mousemove', {
|
||||
clientX: x,
|
||||
clientY: y
|
||||
});
|
||||
|
||||
if (progress >= 1) {
|
||||
options.ondragend && options.ondragend();
|
||||
simulateEvent(toEl, 'mouseup');
|
||||
clearInterval(dragInterval);
|
||||
window.SIMULATE_DRAG_ACTIVE = 0;
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return {
|
||||
target: fromEl,
|
||||
fromList: fromEl.parentNode,
|
||||
toList: toEl.parentNode
|
||||
};
|
||||
if (el.dispatchEvent) {
|
||||
el.dispatchEvent(event);
|
||||
} else if (el.fireEvent) {
|
||||
el.fireEvent(`on${type}`, event);
|
||||
}
|
||||
|
||||
// Export
|
||||
window.simulateEvent = simulateEvent;
|
||||
window.simulateDrag = simulateDrag;
|
||||
})();
|
||||
return event;
|
||||
}
|
||||
|
||||
function isLast(target) {
|
||||
const el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
|
||||
const children = el.children;
|
||||
|
||||
return children.length - 1 === target.index;
|
||||
}
|
||||
|
||||
function getTarget(target) {
|
||||
const el = typeof target.el === 'string' ? document.getElementById(target.el.substr(1)) : target.el;
|
||||
const children = el.children;
|
||||
|
||||
return (
|
||||
children[target.index] ||
|
||||
children[target.index === 'first' ? 0 : -1] ||
|
||||
children[target.index === 'last' ? children.length - 1 : -1] ||
|
||||
el
|
||||
);
|
||||
}
|
||||
|
||||
function getRect(el) {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const width = rect.right - rect.left;
|
||||
const height = (rect.bottom - rect.top) + 10;
|
||||
|
||||
return {
|
||||
x: rect.left,
|
||||
y: rect.top,
|
||||
cx: rect.left + (width / 2),
|
||||
cy: rect.top + (height / 2),
|
||||
w: width,
|
||||
h: height,
|
||||
hw: width / 2,
|
||||
wh: height / 2,
|
||||
};
|
||||
}
|
||||
|
||||
export default function simulateDrag(options) {
|
||||
const { to, from } = options;
|
||||
to.el = to.el || from.el;
|
||||
|
||||
const fromEl = getTarget(from);
|
||||
const toEl = getTarget(to);
|
||||
const firstEl = getTarget({
|
||||
el: to.el,
|
||||
index: 'first',
|
||||
});
|
||||
const lastEl = getTarget({
|
||||
el: options.to.el,
|
||||
index: 'last',
|
||||
});
|
||||
|
||||
const fromRect = getRect(fromEl);
|
||||
const toRect = getRect(toEl);
|
||||
const firstRect = getRect(firstEl);
|
||||
const lastRect = getRect(lastEl);
|
||||
|
||||
const startTime = new Date().getTime();
|
||||
const duration = options.duration || 1000;
|
||||
|
||||
simulateEvent(fromEl, 'mousedown', {
|
||||
button: 0,
|
||||
clientX: fromRect.cx,
|
||||
clientY: fromRect.cy,
|
||||
});
|
||||
|
||||
if (options.ontap) options.ontap();
|
||||
window.SIMULATE_DRAG_ACTIVE = 1;
|
||||
|
||||
if (options.to.index === 0) {
|
||||
toRect.cy = firstRect.y;
|
||||
} else if (isLast(options.to)) {
|
||||
toRect.cy = lastRect.y + lastRect.h + 50;
|
||||
}
|
||||
|
||||
const dragInterval = setInterval(() => {
|
||||
const progress = (new Date().getTime() - startTime) / duration;
|
||||
const x = (fromRect.cx + ((toRect.cx - fromRect.cx) * progress));
|
||||
const y = (fromRect.cy + ((toRect.cy - fromRect.cy) * progress));
|
||||
const overEl = fromEl.ownerDocument.elementFromPoint(x, y);
|
||||
|
||||
simulateEvent(overEl, 'mousemove', {
|
||||
clientX: x,
|
||||
clientY: y,
|
||||
});
|
||||
|
||||
if (progress >= 1) {
|
||||
if (options.ondragend) options.ondragend();
|
||||
simulateEvent(toEl, 'mouseup');
|
||||
clearInterval(dragInterval);
|
||||
window.SIMULATE_DRAG_ACTIVE = 0;
|
||||
}
|
||||
}, 100);
|
||||
|
||||
return {
|
||||
target: fromEl,
|
||||
fromList: fromEl.parentNode,
|
||||
toList: toEl.parentNode,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -38,6 +38,14 @@ export default {
|
|||
new Flash('An error occured while making the request.');
|
||||
});
|
||||
},
|
||||
|
||||
isActionDisabled(action) {
|
||||
if (action.playable === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !action.playable;
|
||||
},
|
||||
},
|
||||
|
||||
template: `
|
||||
|
@ -51,16 +59,23 @@ export default {
|
|||
aria-label="Manual job"
|
||||
:disabled="isLoading">
|
||||
${playIconSvg}
|
||||
<i class="fa fa-caret-down" aria-hidden="true"></i>
|
||||
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
|
||||
<i
|
||||
class="fa fa-caret-down"
|
||||
aria-hidden="true" />
|
||||
<i
|
||||
v-if="isLoading"
|
||||
class="fa fa-spinner fa-spin"
|
||||
aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
<ul class="dropdown-menu dropdown-menu-align-right">
|
||||
<li v-for="action in actions">
|
||||
<button
|
||||
type="button"
|
||||
class="js-pipeline-action-link no-btn"
|
||||
@click="onClickAction(action.path)">
|
||||
class="js-pipeline-action-link no-btn btn"
|
||||
@click="onClickAction(action.path)"
|
||||
:class="{ 'disabled': isActionDisabled(action) }"
|
||||
:disabled="isActionDisabled(action)">
|
||||
${playIconSvg}
|
||||
<span>{{action.name}}</span>
|
||||
</button>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* eslint-disable no-underscore-dangle*/
|
||||
import '../../vue_realtime_listener';
|
||||
import VueRealtimeListener from '../../vue_realtime_listener';
|
||||
|
||||
export default class PipelinesStore {
|
||||
constructor() {
|
||||
|
@ -56,6 +56,6 @@ export default class PipelinesStore {
|
|||
const removeIntervals = () => clearInterval(this.timeLoopInterval);
|
||||
const startIntervals = () => startTimeLoops();
|
||||
|
||||
gl.VueRealtimeListener(removeIntervals, startIntervals);
|
||||
VueRealtimeListener(removeIntervals, startIntervals);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +1,9 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
export default (removeIntervals, startIntervals) => {
|
||||
window.removeEventListener('focus', startIntervals);
|
||||
window.removeEventListener('blur', removeIntervals);
|
||||
window.removeEventListener('onbeforeload', removeIntervals);
|
||||
|
||||
((gl) => {
|
||||
gl.VueRealtimeListener = (removeIntervals, startIntervals) => {
|
||||
const removeAll = () => {
|
||||
removeIntervals();
|
||||
window.removeEventListener('beforeunload', removeIntervals);
|
||||
window.removeEventListener('focus', startIntervals);
|
||||
window.removeEventListener('blur', removeIntervals);
|
||||
document.removeEventListener('beforeunload', removeAll);
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', removeIntervals);
|
||||
window.addEventListener('focus', startIntervals);
|
||||
window.addEventListener('blur', removeIntervals);
|
||||
document.addEventListener('beforeunload', removeAll);
|
||||
|
||||
// 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 = {}));
|
||||
window.addEventListener('focus', startIntervals);
|
||||
window.addEventListener('blur', removeIntervals);
|
||||
window.addEventListener('onbeforeload', removeIntervals);
|
||||
};
|
||||
|
|
|
@ -292,6 +292,10 @@
|
|||
}
|
||||
|
||||
@media(min-width: $screen-xs-max) {
|
||||
&.merge-requests .text-content {
|
||||
margin-top: 40px;
|
||||
}
|
||||
|
||||
&.labels .text-content {
|
||||
margin-top: 70px;
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@
|
|||
}
|
||||
|
||||
.issue-boards-page {
|
||||
.page-with-sidebar {
|
||||
.content-wrapper {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
@ -72,7 +72,7 @@
|
|||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
height: 475px; // Needed for PhantomJS
|
||||
height: calc(100vh - 220px);
|
||||
height: calc(100vh - 222px);
|
||||
min-height: 475px;
|
||||
transition: width .2s;
|
||||
|
||||
|
|
|
@ -159,6 +159,16 @@
|
|||
text {
|
||||
fill: $stat-graph-axis-fill;
|
||||
}
|
||||
|
||||
.label-axis-text,
|
||||
.text-metric-usage {
|
||||
fill: $black;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.legend-axis-text {
|
||||
fill: $black;
|
||||
}
|
||||
}
|
||||
|
||||
.x-axis path,
|
||||
|
|
|
@ -459,20 +459,13 @@ a.deploy-project-label {
|
|||
flex-wrap: wrap;
|
||||
|
||||
.btn {
|
||||
margin: 0 10px 10px 0;
|
||||
padding: 8px;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
> div {
|
||||
margin-bottom: 10px;
|
||||
padding-left: 0;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
|
||||
.btn {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ module ContinueParams
|
|||
|
||||
continue_params = continue_params.permit(:to, :notice, :notice_now)
|
||||
return unless continue_params[:to] && continue_params[:to].start_with?('/')
|
||||
return if continue_params[:to].start_with?('//')
|
||||
|
||||
continue_params
|
||||
end
|
||||
|
|
|
@ -15,6 +15,9 @@ module IssuableCollections
|
|||
# a new order into the collection.
|
||||
# We cannot use reorder to not mess up the paginated collection.
|
||||
issuable_ids = issuable_collection.map(&:id)
|
||||
|
||||
return {} if issuable_ids.empty?
|
||||
|
||||
issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type)
|
||||
issuable_votes_count = AwardEmoji.votes_for_collection(issuable_ids, @collection_type)
|
||||
issuable_merge_requests_count =
|
||||
|
|
|
@ -7,7 +7,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
|
|||
@sort = params[:sort]
|
||||
@todos = @todos.page(params[:page])
|
||||
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
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ class Groups::ApplicationController < ApplicationController
|
|||
unless @group
|
||||
id = params[:group_id] || params[: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)
|
||||
@group = nil
|
||||
|
|
|
@ -1,17 +1,27 @@
|
|||
class Import::BaseController < ApplicationController
|
||||
private
|
||||
|
||||
def find_or_create_namespace(name, owner)
|
||||
return current_user.namespace if name == owner
|
||||
def find_or_create_namespace(names, owner)
|
||||
return current_user.namespace if names == owner
|
||||
return current_user.namespace unless current_user.can_create_group?
|
||||
|
||||
begin
|
||||
name = params[:target_namespace].presence || name
|
||||
namespace = Group.create!(name: name, path: name, owner: current_user)
|
||||
namespace.add_owner(current_user)
|
||||
namespace
|
||||
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
|
||||
Namespace.find_by_full_path(name)
|
||||
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
|
||||
namespace = Group.create!(name: name,
|
||||
path: name,
|
||||
owner: current_user,
|
||||
parent: parent)
|
||||
namespace.add_owner(current_user)
|
||||
|
||||
namespace
|
||||
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
|
||||
Namespace.where(parent: parent).find_by_path_or_name(name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -11,10 +11,10 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
before_action :redirect_to_external_issue_tracker, only: [:index, :new]
|
||||
before_action :module_enabled
|
||||
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
|
||||
before_action :authorize_read_issue!, only: [:show]
|
||||
before_action :authorize_read_issue!, only: [:show, :rendered_title]
|
||||
|
||||
# Allow write(create) issue
|
||||
before_action :authorize_create_issue!, only: [:new, :create]
|
||||
|
@ -31,7 +31,7 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
@issuable_meta_data = issuable_meta_data(@issues, @collection_type)
|
||||
|
||||
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
|
||||
|
||||
if params[:label_name].present?
|
||||
|
@ -200,6 +200,11 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
end
|
||||
end
|
||||
|
||||
def rendered_title
|
||||
Gitlab::PollingInterval.set_header(response, interval: 3_000)
|
||||
render json: { title: view_context.markdown_field(@issue, :title) }
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def issue
|
||||
|
|
|
@ -43,7 +43,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
@issuable_meta_data = issuable_meta_data(@merge_requests, @collection_type)
|
||||
|
||||
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
|
||||
|
||||
if params[:label_name].present?
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
# state: 'open' or 'closed' or 'all'
|
||||
# group_id: integer
|
||||
# project_id: integer
|
||||
# milestone_id: integer
|
||||
# milestone_title: string
|
||||
# assignee_id: integer
|
||||
# search: string
|
||||
# label_name: string
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
# state: 'open' or 'closed' or 'all'
|
||||
# group_id: integer
|
||||
# project_id: integer
|
||||
# milestone_id: integer
|
||||
# milestone_title: string
|
||||
# assignee_id: integer
|
||||
# search: string
|
||||
# label_name: string
|
||||
|
|
|
@ -407,7 +407,10 @@ module ProjectsHelper
|
|||
def sanitize_repo_path(project, message)
|
||||
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
|
||||
|
||||
def project_feature_options
|
||||
|
|
|
@ -46,10 +46,18 @@ class Blob < SimpleDelegator
|
|||
text? && language && language.name == 'SVG'
|
||||
end
|
||||
|
||||
def pdf?
|
||||
name && File.extname(name) == '.pdf'
|
||||
end
|
||||
|
||||
def ipython_notebook?
|
||||
text? && language&.name == 'Jupyter Notebook'
|
||||
end
|
||||
|
||||
def sketch?
|
||||
binary? && extname.downcase.delete('.') == 'sketch'
|
||||
end
|
||||
|
||||
def size_within_svg_limits?
|
||||
size <= MAXIMUM_SVG_SIZE
|
||||
end
|
||||
|
@ -67,8 +75,12 @@ class Blob < SimpleDelegator
|
|||
end
|
||||
elsif image? || svg?
|
||||
'image'
|
||||
elsif pdf?
|
||||
'pdf'
|
||||
elsif ipython_notebook?
|
||||
'notebook'
|
||||
elsif sketch?
|
||||
'sketch'
|
||||
elsif text?
|
||||
'text'
|
||||
else
|
||||
|
|
|
@ -540,6 +540,8 @@ module Ci
|
|||
end
|
||||
|
||||
def dependencies
|
||||
return [] if empty_dependencies?
|
||||
|
||||
depended_jobs = depends_on_builds
|
||||
|
||||
return depended_jobs unless options[:dependencies].present?
|
||||
|
@ -549,6 +551,10 @@ module Ci
|
|||
end
|
||||
end
|
||||
|
||||
def empty_dependencies?
|
||||
options[:dependencies]&.empty?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def update_artifacts_size
|
||||
|
|
|
@ -40,6 +40,8 @@ class Issue < ActiveRecord::Base
|
|||
|
||||
scope :include_associations, -> { includes(:assignee, :labels, project: :namespace) }
|
||||
|
||||
after_save :expire_etag_cache
|
||||
|
||||
attr_spammable :title, spam_title: true
|
||||
attr_spammable :description, spam_description: true
|
||||
|
||||
|
@ -59,10 +61,6 @@ class Issue < ActiveRecord::Base
|
|||
before_transition any => :closed do |issue|
|
||||
issue.closed_at = Time.zone.now
|
||||
end
|
||||
|
||||
before_transition closed: any do |issue|
|
||||
issue.closed_at = nil
|
||||
end
|
||||
end
|
||||
|
||||
def hook_attrs
|
||||
|
@ -256,4 +254,13 @@ class Issue < ActiveRecord::Base
|
|||
def publicly_visible?
|
||||
project.public? && !confidential?
|
||||
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
|
||||
|
|
|
@ -177,6 +177,16 @@ class MergeRequestDiff < ActiveRecord::Base
|
|||
st_commits.count
|
||||
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
|
||||
|
||||
# Old GitLab implementations may have generated diffs as ["--broken-diff"].
|
||||
|
@ -270,14 +280,6 @@ class MergeRequestDiff < ActiveRecord::Base
|
|||
project.merge_base_commit(head_commit_sha, start_commit_sha).try(:sha)
|
||||
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
|
||||
# serialization and deserialization calls resulting in bad performance.
|
||||
|
|
|
@ -150,7 +150,7 @@ class Namespace < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def any_project_has_container_registry_tags?
|
||||
projects.any?(&:has_container_registry_tags?)
|
||||
all_projects.any?(&:has_container_registry_tags?)
|
||||
end
|
||||
|
||||
def send_update_instructions
|
||||
|
@ -214,6 +214,12 @@ class Namespace < ActiveRecord::Base
|
|||
@old_repository_storage_paths ||= repository_storage_paths
|
||||
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
|
||||
|
||||
def repository_storage_paths
|
||||
|
@ -221,7 +227,7 @@ class Namespace < ActiveRecord::Base
|
|||
# pending delete. Unscoping also get rids of the default order, which causes
|
||||
# problems with SELECT DISTINCT.
|
||||
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
|
||||
|
||||
|
|
|
@ -114,6 +114,8 @@ class Project < ActiveRecord::Base
|
|||
has_one :kubernetes_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_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_from_project, through: :forked_project_link
|
||||
|
|
|
@ -22,22 +22,21 @@ class KubernetesService < DeploymentService
|
|||
with_options presence: true, if: :activated? do
|
||||
validates :api_url, url: true
|
||||
validates :token
|
||||
|
||||
validates :namespace,
|
||||
format: {
|
||||
with: Gitlab::Regex.kubernetes_namespace_regex,
|
||||
message: Gitlab::Regex.kubernetes_namespace_regex_message,
|
||||
},
|
||||
length: 1..63
|
||||
end
|
||||
|
||||
validates :namespace,
|
||||
allow_blank: true,
|
||||
length: 1..63,
|
||||
if: :activated?,
|
||||
format: {
|
||||
with: Gitlab::Regex.kubernetes_namespace_regex,
|
||||
message: Gitlab::Regex.kubernetes_namespace_regex_message
|
||||
}
|
||||
|
||||
after_save :clear_reactive_cache!
|
||||
|
||||
def initialize_properties
|
||||
if properties.nil?
|
||||
self.properties = {}
|
||||
self.namespace = "#{project.path}-#{project.id}" if project.present?
|
||||
end
|
||||
self.properties = {} if properties.nil?
|
||||
end
|
||||
|
||||
def title
|
||||
|
@ -62,7 +61,7 @@ class KubernetesService < DeploymentService
|
|||
{ type: 'text',
|
||||
name: 'namespace',
|
||||
title: 'Kubernetes namespace',
|
||||
placeholder: 'Kubernetes namespace' },
|
||||
placeholder: namespace_placeholder },
|
||||
{ type: 'text',
|
||||
name: 'api_url',
|
||||
title: 'API URL',
|
||||
|
@ -92,7 +91,7 @@ class KubernetesService < DeploymentService
|
|||
variables = [
|
||||
{ key: 'KUBE_URL', value: api_url, public: true },
|
||||
{ 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?
|
||||
|
@ -135,8 +134,26 @@ class KubernetesService < DeploymentService
|
|||
{ pods: pods }
|
||||
end
|
||||
|
||||
TEMPLATE_PLACEHOLDER = 'Kubernetes namespace'.freeze
|
||||
|
||||
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')
|
||||
raise "Incomplete settings" unless api_url && namespace && token
|
||||
|
||||
|
|
18
app/models/project_services/mock_deployment_service.rb
Normal file
|
@ -0,0 +1,18 @@
|
|||
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
|
17
app/models/project_services/mock_monitoring_service.rb
Normal file
|
@ -0,0 +1,17 @@
|
|||
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
|
|||
def raw_repository
|
||||
return nil unless path_with_namespace
|
||||
|
||||
@raw_repository ||= Gitlab::Git::Repository.new(path_to_repo)
|
||||
@raw_repository ||= initialize_raw_repository
|
||||
end
|
||||
|
||||
# Return absolute path to repository
|
||||
|
@ -146,12 +146,7 @@ class Repository
|
|||
# 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
|
||||
raw_repo =
|
||||
if fresh_repo
|
||||
Gitlab::Git::Repository.new(path_to_repo)
|
||||
else
|
||||
raw_repository
|
||||
end
|
||||
raw_repo = fresh_repo ? initialize_raw_repository : raw_repository
|
||||
|
||||
raw_repo.find_branch(name)
|
||||
end
|
||||
|
@ -505,9 +500,7 @@ class Repository
|
|||
end
|
||||
end
|
||||
|
||||
def branch_names
|
||||
branches.map(&:name)
|
||||
end
|
||||
delegate :branch_names, to: :raw_repository
|
||||
cache_method :branch_names, fallback: []
|
||||
|
||||
delegate :tag_names, to: :raw_repository
|
||||
|
@ -1168,4 +1161,8 @@ class Repository
|
|||
def repository_storage_path
|
||||
@project.repository_storage_path
|
||||
end
|
||||
|
||||
def initialize_raw_repository
|
||||
Gitlab::Git::Repository.new(project.repository_storage, path_with_namespace + '.git')
|
||||
end
|
||||
end
|
||||
|
|
|
@ -238,7 +238,9 @@ class Service < ActiveRecord::Base
|
|||
slack
|
||||
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)
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
class SystemNoteMetadata < ActiveRecord::Base
|
||||
ICON_TYPES = %w[
|
||||
commit merge confidentiality status label assignee cross_reference
|
||||
title time_tracking branch milestone discussion task moved
|
||||
commit merge confidential visible label assignee cross_reference
|
||||
title time_tracking branch milestone discussion task moved opened closed merged
|
||||
].freeze
|
||||
|
||||
validates :note, presence: true
|
||||
|
|
|
@ -11,4 +11,6 @@ class BuildActionEntity < Grape::Entity
|
|||
build.project,
|
||||
build)
|
||||
end
|
||||
|
||||
expose :playable?, as: :playable
|
||||
end
|
||||
|
|
|
@ -16,6 +16,7 @@ class BuildEntity < Grape::Entity
|
|||
path_to(:play_namespace_project_build, build)
|
||||
end
|
||||
|
||||
expose :playable?, as: :playable
|
||||
expose :created_at
|
||||
expose :updated_at
|
||||
expose :detailed_status, as: :status, with: StatusEntity
|
||||
|
|
|
@ -21,7 +21,9 @@ module MergeRequests
|
|||
delegate :target_branch, :source_branch, :source_project, :target_project, :compare_commits, :wip_title, :description, :errors, to: :merge_request
|
||||
|
||||
def find_source_project
|
||||
source_project || project
|
||||
return source_project if source_project.present? && can?(current_user, :read_project, source_project)
|
||||
|
||||
project
|
||||
end
|
||||
|
||||
def find_target_project
|
||||
|
|
|
@ -18,7 +18,11 @@ module Search
|
|||
end
|
||||
|
||||
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
|
||||
|
|
|
@ -183,7 +183,9 @@ module SystemNoteService
|
|||
body = status.dup
|
||||
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
|
||||
|
||||
# Called when 'merge when pipeline succeeds' is executed
|
||||
|
@ -273,9 +275,15 @@ module SystemNoteService
|
|||
#
|
||||
# Returns the created Note object
|
||||
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
|
||||
|
||||
# Called when a branch in Noteable is changed
|
||||
|
|
|
@ -62,6 +62,7 @@ module Users
|
|||
:email,
|
||||
:external,
|
||||
:force_random_password,
|
||||
:password_automatically_set,
|
||||
:hide_no_password,
|
||||
:hide_no_ssh_key,
|
||||
:key_id,
|
||||
|
@ -85,6 +86,7 @@ module Users
|
|||
[
|
||||
:email,
|
||||
:email_confirmation,
|
||||
:password_automatically_set,
|
||||
:name,
|
||||
:password,
|
||||
:username
|
||||
|
|
|
@ -1,18 +1,22 @@
|
|||
- page_title "Merge Requests"
|
||||
|
||||
.top-area
|
||||
= render 'shared/issuable/nav', type: :merge_requests
|
||||
- if current_user
|
||||
.nav-controls
|
||||
= render 'shared/new_project_item_select', path: 'merge_requests/new', label: "New Merge Request"
|
||||
- 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
|
||||
- if current_user
|
||||
.nav-controls
|
||||
= 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
|
||||
Only merge requests from
|
||||
%strong= @group.name
|
||||
group are listed here.
|
||||
- if current_user
|
||||
To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page.
|
||||
.row-content-block.second-block
|
||||
Only merge requests from
|
||||
%strong= @group.name
|
||||
group are listed here.
|
||||
- if current_user
|
||||
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'
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
= 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/tabs', milestone: @milestone, show_project_name: true
|
||||
= render 'shared/milestones/sidebar', milestone: @milestone, affix_offset: 102
|
||||
|
|
5
app/views/projects/blob/_pdf.html.haml
Normal file
|
@ -0,0 +1,5 @@
|
|||
- 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) } }
|
7
app/views/projects/blob/_sketch.html.haml
Normal file
|
@ -0,0 +1,7 @@
|
|||
- 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 @@
|
|||
= page_specific_javascript_bundle_tag('common_vue')
|
||||
= page_specific_javascript_bundle_tag('filtered_search')
|
||||
= 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-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
|
||||
|
||||
= render "projects/issues/head"
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
.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 @@
|
|||
%ul
|
||||
%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.
|
||||
- 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"
|
||||
- if can?(current_user, :change_namespace, @project)
|
||||
%hr
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
- 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
|
||||
= icon('external-link')
|
||||
View deployment
|
||||
|
|
|
@ -4,3 +4,4 @@
|
|||
|
||||
= link_to environment_metrics_path(environment), title: 'See metrics', class: 'btn metrics-button' do
|
||||
= icon('area-chart')
|
||||
Monitoring
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
.col-sm-6
|
||||
%h3.page-title
|
||||
Environment:
|
||||
= @environment.name
|
||||
= link_to @environment.name, environment_path(@environment)
|
||||
|
||||
.col-sm-6
|
||||
.nav-controls
|
||||
|
|
|
@ -4,9 +4,9 @@
|
|||
|
||||
%div{ class: container_class }
|
||||
.top-area.adjust
|
||||
.col-md-9
|
||||
.col-md-7
|
||||
%h3.page-title= @environment.name
|
||||
.col-md-3
|
||||
.col-md-5
|
||||
.nav-controls
|
||||
= render 'projects/environments/metrics_button', environment: @environment
|
||||
= render 'projects/environments/terminal_button', environment: @environment
|
||||
|
|
|
@ -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 'Edit', edit_namespace_project_issue_path(@project.namespace, @project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
|
||||
|
||||
|
||||
.issue-details.issuable-details
|
||||
.detail-page-description.content-block{ class: ('hide-bottom-border' unless @issue.description.present? ) }
|
||||
%h2.title
|
||||
= markdown_field(@issue, :title)
|
||||
.issue-title-data.hidden{ "data" => { "initial-title" => markdown_field(@issue, :title),
|
||||
"endpoint" => rendered_title_namespace_project_issue_path(@project.namespace, @project, @issue),
|
||||
} }
|
||||
.issue-title-entrypoint
|
||||
- if @issue.description.present?
|
||||
.description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' }
|
||||
.wiki
|
||||
|
@ -77,3 +78,5 @@
|
|||
= render 'projects/issues/discussion'
|
||||
|
||||
= render 'shared/issuable/sidebar', issuable: @issue
|
||||
|
||||
= page_specific_javascript_bundle_tag('issue_show')
|
||||
|
|
|
@ -3,9 +3,6 @@
|
|||
- hide_class = ''
|
||||
= 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?
|
||||
%div{ class: container_class }
|
||||
.top-area.adjust
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
%ul.content-list.mr-list.issuable-list
|
||||
= render @merge_requests
|
||||
- if @merge_requests.blank?
|
||||
%li
|
||||
.nothing-here-block No merge requests to show
|
||||
- if @merge_requests.exists?
|
||||
= render @merge_requests
|
||||
- else
|
||||
= render 'shared/empty_states/merge_requests'
|
||||
|
||||
- if @merge_requests.present?
|
||||
= paginate @merge_requests, theme: "gitlab"
|
||||
|
|
|
@ -7,16 +7,19 @@
|
|||
- content_for :page_specific_javascripts do
|
||||
= page_specific_javascript_bundle_tag('filtered_search')
|
||||
|
||||
%div{ class: container_class }
|
||||
.top-area
|
||||
= render 'shared/issuable/nav', type: :merge_requests
|
||||
.nav-controls
|
||||
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@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
|
||||
New Merge Request
|
||||
- if @project.merge_requests.exists?
|
||||
%div{ class: container_class }
|
||||
.top-area
|
||||
= render 'shared/issuable/nav', type: :merge_requests
|
||||
.nav-controls
|
||||
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@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
|
||||
New merge request
|
||||
|
||||
= render 'shared/issuable/search_bar', type: :merge_requests
|
||||
= render 'shared/issuable/search_bar', type: :merge_requests
|
||||
|
||||
.merge-requests-holder
|
||||
= render 'merge_requests'
|
||||
.merge-requests-holder
|
||||
= 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 @@
|
|||
- page_description @milestone.description
|
||||
= 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 }
|
||||
.detail-page-header.milestone-page-header
|
||||
.status-box{ class: status_box_class(@milestone) }
|
||||
|
|
|
@ -78,7 +78,7 @@
|
|||
- if git_import_enabled?
|
||||
%button.btn.js-toggle-button.import_git{ type: "button" }
|
||||
= icon('git', text: 'Repo by URL')
|
||||
.import_gitlab_project
|
||||
.import_gitlab_project.has-tooltip{ data: { container: 'body' } }
|
||||
- if gitlab_project_import_enabled?
|
||||
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
|
||||
= icon('gitlab', text: 'GitLab export')
|
||||
|
@ -109,6 +109,9 @@
|
|||
%p Please wait a moment, this page will automatically refresh when ready.
|
||||
|
||||
:javascript
|
||||
var importBtnTooltip = "Please enter a valid project name.";
|
||||
var $importBtnWrapper = $('.import_gitlab_project');
|
||||
|
||||
$('.how_to_import_link').bind('click', function (e) {
|
||||
e.preventDefault();
|
||||
var import_modal = $(this).next(".modal").show();
|
||||
|
@ -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('disabled',true)
|
||||
$('.import_gitlab_project').attr('title', 'Project path and name required.');
|
||||
|
||||
$('.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.");
|
||||
}
|
||||
});
|
||||
$('.btn_import_gitlab_project').attr('disabled', $('#project_path').val().trim().length === 0);
|
||||
$importBtnWrapper.attr('title', importBtnTooltip);
|
||||
|
||||
$('#new_project').submit(function(){
|
||||
var $path = $('#project_path');
|
||||
|
@ -139,13 +135,13 @@
|
|||
});
|
||||
|
||||
$('#project_path').keyup(function(){
|
||||
if($(this).val().length !=0) {
|
||||
if($(this).val().trim().length !== 0) {
|
||||
$('.btn_import_gitlab_project').attr('disabled', false);
|
||||
$('.import_gitlab_project').attr('title','');
|
||||
$(".flash-container").html("")
|
||||
$importBtnWrapper.attr('title','');
|
||||
$importBtnWrapper.removeClass('has-tooltip');
|
||||
} else {
|
||||
$('.btn_import_gitlab_project').attr('disabled',true);
|
||||
$('.import_gitlab_project').attr('title', 'Project path and name required.');
|
||||
$importBtnWrapper.addClass('has-tooltip');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
- 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_errors(@page)
|
||||
|
||||
|
@ -28,7 +30,7 @@
|
|||
|
||||
.form-group
|
||||
= 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
|
||||
- if @page && @page.persisted?
|
||||
|
|
|
@ -6,4 +6,4 @@
|
|||
= paginate @merge_requests, theme: "gitlab"
|
||||
|
||||
- else
|
||||
.nothing-here-block No merge requests to show
|
||||
= render 'shared/empty_states/merge_requests'
|
||||
|
|
22
app/views/shared/empty_states/_merge_requests.html.haml
Normal file
|
@ -0,0 +1,22 @@
|
|||
- 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.
|
1
app/views/shared/empty_states/icons/_merge_requests.svg
Normal file
After Width: | Height: | Size: 7.5 KiB |
|
@ -1,16 +0,0 @@
|
|||
<?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>
|
Before Width: | Height: | Size: 1.2 KiB |
|
@ -1,10 +0,0 @@
|
|||
<?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>
|
Before Width: | Height: | Size: 1.2 KiB |
|
@ -1,17 +0,0 @@
|
|||
<?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>
|
Before Width: | Height: | Size: 2.6 KiB |
|
@ -1,3 +0,0 @@
|
|||
<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>
|
Before Width: | Height: | Size: 549 B |
|
@ -1,17 +0,0 @@
|
|||
<?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>
|
Before Width: | Height: | Size: 1.5 KiB |
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><path d="M9,7.5l5.83-5.91a.48.48,0,0,0,0-.69L14.11.15a.46.46,0,0,0-.68,0l-5.93,6L1.57.15a.46.46,0,0,0-.68,0L.15.9a.48.48,0,0,0,0,.69L6,7.5.15,13.41a.48.48,0,0,0,0,.69l.74.75a.46.46,0,0,0,.68,0l5.93-6,5.93,6a.46.46,0,0,0,.68,0l.74-.75a.48.48,0,0,0,0-.69Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><path d="M9,7.5l5.83-5.91a.48.48,0,0,0,0-.69L14.11.15a.46.46,0,0,0-.68,0l-5.93,6L1.57.15a.46.46,0,0,0-.68,0L.15.9a.48.48,0,0,0,0,.69L6,7.5.15,13.41a.48.48,0,0,0,0,.69l.74.75a.46.46,0,0,0,.68,0l5.93-6,5.93,6a.46.46,0,0,0,.68,0l.74-.75a.48.48,0,0,0,0-.69Z"/></svg>
|
||||
|
|
Before Width: | Height: | Size: 322 B After Width: | Height: | Size: 323 B |
Before Width: | Height: | Size: 8.1 KiB After Width: | Height: | Size: 8.1 KiB |
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill-rule="evenodd"><path d="m8.411 1.012c-.136-.008-.273-.012-.411-.012-3.866 0-7 3.134-7 7 0 3.866 3.134 7 7 7 3.866 0 7-3.134 7-7 0-.138-.004-.275-.012-.411-.464.201-.964.334-1.488.386 0 .008 0 .016 0 .025 0 3.038-2.462 5.5-5.5 5.5-3.038 0-5.5-2.462-5.5-5.5 0-3.038 2.462-5.5 5.5-5.5.008 0 .016 0 .025 0 .052-.524.185-1.024.386-1.488"/><path d="m12 2h-1.01c-.54 0-.991.448-.991 1 0 .556.444 1 .991 1h1.01v1.01c0 .54.448.991 1 .991.556 0 1-.444 1-.991v-1.01h1.01c.54 0 .991-.448.991-1 0-.556-.444-1-.991-1h-1.01v-1.01c0-.54-.448-.991-1-.991-.556 0-1 .444-1 .991v1.01m-5 4.01c0-.557.444-1.01 1-1.01.552 0 1 .443 1 1.01v1.981c0 .557-.444 1.01-1 1.01-.552 0-1-.443-1-1.01v-1.981m1 5.991c.552 0 1-.448 1-1 0-.552-.448-1-1-1-.552 0-1 .448-1 1 0 .552.448 1 1 1"/></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g fill-rule="evenodd"><path d="m8.411 1.012c-.136-.008-.273-.012-.411-.012-3.866 0-7 3.134-7 7 0 3.866 3.134 7 7 7 3.866 0 7-3.134 7-7 0-.138-.004-.275-.012-.411-.464.201-.964.334-1.488.386 0 .008 0 .016 0 .025 0 3.038-2.462 5.5-5.5 5.5-3.038 0-5.5-2.462-5.5-5.5 0-3.038 2.462-5.5 5.5-5.5.008 0 .016 0 .025 0 .052-.524.185-1.024.386-1.488"/><path d="m12 2h-1.01c-.54 0-.991.448-.991 1 0 .556.444 1 .991 1h1.01v1.01c0 .54.448.991 1 .991.556 0 1-.444 1-.991v-1.01h1.01c.54 0 .991-.448.991-1 0-.556-.444-1-.991-1h-1.01v-1.01c0-.54-.448-.991-1-.991-.556 0-1 .444-1 .991v1.01m-5 4.01c0-.557.444-1.01 1-1.01.552 0 1 .443 1 1.01v1.981c0 .557-.444 1.01-1 1.01-.552 0-1-.443-1-1.01v-1.981m1 5.991c.552 0 1-.448 1-1 0-.552-.448-1-1-1-.552 0-1 .448-1 1 0 .552.448 1 1 1"/></g></svg>
|
||||
|
|
Before Width: | Height: | Size: 832 B After Width: | Height: | Size: 833 B |
|
@ -1,3 +1 @@
|
|||
<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>
|
||||
<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>
|
||||
|
|
Before Width: | Height: | Size: 251 B After Width: | Height: | Size: 246 B |
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 14" enable-background="new 0 0 12 14"><path d="m11.5 2.4l-1.3-1.1-1 1.1 1.4 1.1.9-1.1"/><path d="m6.8 2v-.5h.5v-1.5h-2.6v1.5h.5v.5c-2.9.4-5.2 2.9-5.2 6 0 3.3 2.7 6 6 6s6-2.7 6-6c0-3-2.3-5.6-5.2-6m-.8 10.5c-2.5 0-4.5-2-4.5-4.5s2-4.5 4.5-4.5 4.5 2 4.5 4.5-2 4.5-4.5 4.5"/><path d="m6.2 8.9h-.5c-.1 0-.2-.1-.2-.2v-3.5c0-.1.1-.2.2-.2h.5c.1 0 .2.1.2.2v3.5c0 .1-.1.2-.2.2"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 14" enable-background="new 0 0 12 14"><path d="m11.5 2.4l-1.3-1.1-1 1.1 1.4 1.1.9-1.1"/><path d="m6.8 2v-.5h.5v-1.5h-2.6v1.5h.5v.5c-2.9.4-5.2 2.9-5.2 6 0 3.3 2.7 6 6 6s6-2.7 6-6c0-3-2.3-5.6-5.2-6m-.8 10.5c-2.5 0-4.5-2-4.5-4.5s2-4.5 4.5-4.5 4.5 2 4.5 4.5-2 4.5-4.5 4.5"/><path d="m6.2 8.9h-.5c-.1 0-.2-.1-.2-.2v-3.5c0-.1.1-.2.2-.2h.5c.1 0 .2.1.2.2v3.5c0 .1-.1.2-.2.2"/></svg>
|
||||
|
|
Before Width: | Height: | Size: 430 B After Width: | Height: | Size: 431 B |
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><g fill="#8F8F8F" fill-rule="evenodd"><path d="M29.513 10.134A15.922 15.922 0 0 0 23 7.28V6h2.993C26.55 6 27 5.552 27 5V2a1 1 0 0 0-1.007-1H14.007C13.45 1 13 1.448 13 2v3a1 1 0 0 0 1.007 1H17v1.28C9.597 8.686 4 15.19 4 23c0 8.837 7.163 16 16 16s16-7.163 16-16c0-3.461-1.099-6.665-2.967-9.283l1.327-1.58a2.498 2.498 0 0 0-.303-3.53 2.499 2.499 0 0 0-3.528.315l-1.016 1.212zM20 34c6.075 0 11-4.925 11-11s-4.925-11-11-11S9 16.925 9 23s4.925 11 11 11z"/><path d="M19 21h-4.002c-.552 0-.998.452-.998 1.01v1.98c0 .567.447 1.01.998 1.01h7.004c.274 0 .521-.111.701-.291a.979.979 0 0 0 .297-.704v-8.01c0-.54-.452-.995-1.01-.995h-1.98a.997.997 0 0 0-1.01.995V21z"/></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 40 40"><g fill="#8F8F8F" fill-rule="evenodd"><path d="M29.513 10.134A15.922 15.922 0 0 0 23 7.28V6h2.993C26.55 6 27 5.552 27 5V2a1 1 0 0 0-1.007-1H14.007C13.45 1 13 1.448 13 2v3a1 1 0 0 0 1.007 1H17v1.28C9.597 8.686 4 15.19 4 23c0 8.837 7.163 16 16 16s16-7.163 16-16c0-3.461-1.099-6.665-2.967-9.283l1.327-1.58a2.498 2.498 0 0 0-.303-3.53 2.499 2.499 0 0 0-3.528.315l-1.016 1.212zM20 34c6.075 0 11-4.925 11-11s-4.925-11-11-11S9 16.925 9 23s4.925 11 11 11z"/><path d="M19 21h-4.002c-.552 0-.998.452-.998 1.01v1.98c0 .567.447 1.01.998 1.01h7.004c.274 0 .521-.111.701-.291a.979.979 0 0 0 .297-.704v-8.01c0-.54-.452-.995-1.01-.995h-1.98a.997.997 0 0 0-1.01.995V21z"/></g></svg>
|
||||
|
|
Before Width: | Height: | Size: 748 B After Width: | Height: | Size: 749 B |
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 168 107" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="#eee" fill-rule="evenodd"><path d="m4.01 2h1.102c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-1.102c-2.218 0-4.01 1.788-4.01 4 0 .552.448 1 1 1 .552 0 1-.448 1-1 0-1.108.892-2 2.01-2m12.702 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m8.088 0c.822 0 1.554.503 1.86 1.254.208.512.791.758 1.303.55.512-.208.758-.791.55-1.303-.609-1.497-2.069-2.5-3.712-2.5h-2.188c-.552 0-1 .448-1 1 0 .552.448 1 1 1h2.188m2.01 12.518c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 11.6c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 11.6c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 6.282c0 1.108-.892 2-2.01 2h-.72c-.552 0-1 .448-1 1 0 .552.448 1 1 1h.72c2.218 0 4.01-1.788 4.01-4v-.382c0-.552-.448-1-1-1-.552 0-1 .448-1 1v.382m-14.325 2c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-8.47 0c-.755 0-1.438-.424-1.782-1.085-.255-.49-.859-.681-1.349-.426-.49.255-.681.859-.426 1.349.684 1.316 2.046 2.162 3.556 2.162h2.57c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-2.57m-2.01-12.136c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-11.6c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-11.6c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-6.664c0-.552-.448-1-1-1-.552 0-1 .448-1 1v.764c0 .552.448 1 1 1 .552 0 1-.448 1-1v-.764" id="0"/><circle cx="21" cy="24" r="10"/><rect width="33" height="3" x="37" y="18" rx="1.5" id="1"/><rect width="53" height="3" x="37" y="27" rx="1.5" id="2"/><path d="m131 29c0 .552.447.999.996.999h22.01c.545 0 .996-.451.996-.999v-9c0-.552-.447-.999-.996-.999h-22.01c-.545 0-.996.451-.996.999v9m.996-12h22.01c1.655 0 2.996 1.344 2.996 2.999v9c0 1.657-1.35 2.999-2.996 2.999h-22.01c-1.655 0-2.996-1.344-2.996-2.999v-9c0-1.657 1.35-2.999 2.996-2.999" id="3"/><g transform="translate(0 59)"><use xlink:href="#0"/><circle cx="21" cy="24" r="10"/><use xlink:href="#1"/><use xlink:href="#2"/><use xlink:href="#3"/></g></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 168 107" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="#eee" fill-rule="evenodd"><path d="m4.01 2h1.102c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-1.102c-2.218 0-4.01 1.788-4.01 4 0 .552.448 1 1 1 .552 0 1-.448 1-1 0-1.108.892-2 2.01-2m12.702 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m8.088 0c.822 0 1.554.503 1.86 1.254.208.512.791.758 1.303.55.512-.208.758-.791.55-1.303-.609-1.497-2.069-2.5-3.712-2.5h-2.188c-.552 0-1 .448-1 1 0 .552.448 1 1 1h2.188m2.01 12.518c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 11.6c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 11.6c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 6.282c0 1.108-.892 2-2.01 2h-.72c-.552 0-1 .448-1 1 0 .552.448 1 1 1h.72c2.218 0 4.01-1.788 4.01-4v-.382c0-.552-.448-1-1-1-.552 0-1 .448-1 1v.382m-14.325 2c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-8.47 0c-.755 0-1.438-.424-1.782-1.085-.255-.49-.859-.681-1.349-.426-.49.255-.681.859-.426 1.349.684 1.316 2.046 2.162 3.556 2.162h2.57c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-2.57m-2.01-12.136c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-11.6c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-11.6c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-6.664c0-.552-.448-1-1-1-.552 0-1 .448-1 1v.764c0 .552.448 1 1 1 .552 0 1-.448 1-1v-.764" id="0"/><circle cx="21" cy="24" r="10"/><rect width="33" height="3" x="37" y="18" rx="1.5" id="1"/><rect width="53" height="3" x="37" y="27" rx="1.5" id="2"/><path d="m131 29c0 .552.447.999.996.999h22.01c.545 0 .996-.451.996-.999v-9c0-.552-.447-.999-.996-.999h-22.01c-.545 0-.996.451-.996.999v9m.996-12h22.01c1.655 0 2.996 1.344 2.996 2.999v9c0 1.657-1.35 2.999-2.996 2.999h-22.01c-1.655 0-2.996-1.344-2.996-2.999v-9c0-1.657 1.35-2.999 2.996-2.999" id="3"/><g transform="translate(0 59)"><use xlink:href="#0"/><circle cx="21" cy="24" r="10"/><use xlink:href="#1"/><use xlink:href="#2"/><use xlink:href="#3"/></g></g></svg>
|
||||
|
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
|
@ -1,13 +0,0 @@
|
|||
<?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>
|
Before Width: | Height: | Size: 2 KiB |
|
@ -1,15 +0,0 @@
|
|||
<?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>
|
Before Width: | Height: | Size: 1.7 KiB |
|
@ -1,13 +0,0 @@
|
|||
<?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>
|
Before Width: | Height: | Size: 1.1 KiB |