Commit 9ffb60ac authored by Phil Hughes's avatar Phil Hughes

Merge branch 'master' into ide-list-merge-requests

parents 8868c919 e206e328
...@@ -8,8 +8,6 @@ engines: ...@@ -8,8 +8,6 @@ engines:
languages: languages:
- ruby - ruby
- javascript - javascript
exclude_paths:
- "lib/api/v3/*"
ratings: ratings:
paths: paths:
- Gemfile.lock - Gemfile.lock
......
{
"env": {
"browser": true,
"es6": true
},
"extends": [
"airbnb-base",
"plugin:vue/recommended"
],
"globals": {
"__webpack_public_path__": true,
"gl": false,
"gon": false,
"localStorage": false
},
"parserOptions": {
"parser": "babel-eslint"
},
"plugins": [
"filenames",
"import",
"html",
"promise"
],
"settings": {
"html/html-extensions": [".html", ".html.raw"],
"import/resolver": {
"webpack": {
"config": "./config/webpack.config.js"
}
}
},
"rules": {
"filenames/match-regex": [2, "^[a-z0-9_]+$"],
"import/no-commonjs": "error",
"no-multiple-empty-lines": ["error", { "max": 1 }],
"promise/catch-or-return": "error",
"no-underscore-dangle": ["error", { "allow": ["__", "_links"] }],
"no-mixed-operators": 0,
"space-before-function-paren": 0,
"curly": 0,
"arrow-parens": 0,
"vue/html-self-closing": [
"error",
{
"html": {
"void": "always",
"normal": "never",
"component": "always"
},
"svg": "always",
"math": "always"
}
]
}
}
---
env:
browser: true
es6: true
extends:
- airbnb-base
- plugin:vue/recommended
globals:
__webpack_public_path__: true
gl: false
gon: false
localStorage: false
parserOptions:
parser: babel-eslint
plugins:
- filenames
- import
- html
- promise
settings:
html/html-extensions:
- ".html"
- ".html.raw"
import/resolver:
webpack:
config: "./config/webpack.config.js"
rules:
filenames/match-regex:
- error
- "^[a-z0-9_]+$"
import/no-commonjs: error
no-multiple-empty-lines:
- error
- max: 1
promise/catch-or-return: error
no-underscore-dangle:
- error
- allow:
- __
- _links
no-mixed-operators: off
vue/html-self-closing:
- error
- html:
void: always
normal: never
component: always
svg: always
math: always
## Conflicting rules with prettier:
space-before-function-paren: off
curly: off
arrow-parens: off
function-paren-newline: off
object-curly-newline: off
padded-blocks: off
# Disabled for now, to make the eslint 3 -> eslint 4 update smoother
## Indent rule. We are using the old for now: https://eslint.org/docs/user-guide/migrating-to-4.0.0#indent-rewrite
indent: off
indent-legacy:
- error
- 2
- SwitchCase: 1
VariableDeclarator: 1
outerIIFEBody: 1
FunctionDeclaration:
parameters: 1
body: 1
FunctionExpression:
parameters: 1
body: 1
## Destructuring: https://eslint.org/docs/rules/prefer-destructuring
prefer-destructuring: off
## no-restricted-globals: https://eslint.org/docs/rules/no-restricted-globals
no-restricted-globals: off
## no-multi-assign: https://eslint.org/docs/rules/no-multi-assign
no-multi-assign: off
...@@ -64,6 +64,7 @@ eslint-report.html ...@@ -64,6 +64,7 @@ eslint-report.html
/tags /tags
/tmp/* /tmp/*
/vendor/bundle/* /vendor/bundle/*
/vendor/gitaly-ruby
/builds* /builds*
/shared/* /shared/*
/.gitlab_workhorse_secret /.gitlab_workhorse_secret
......
...@@ -591,7 +591,7 @@ ee_compat_check: ...@@ -591,7 +591,7 @@ ee_compat_check:
except: except:
- master - master
- tags - tags
- /^[\d-]+-stable(-ee)?/ - /[\d-]+-stable(-ee)?/
- /^security-/ - /^security-/
- branches@gitlab-org/gitlab-ee - branches@gitlab-org/gitlab-ee
- branches@gitlab/gitlab-ee - branches@gitlab/gitlab-ee
......
...@@ -173,7 +173,6 @@ Lint/UriEscapeUnescape: ...@@ -173,7 +173,6 @@ Lint/UriEscapeUnescape:
- 'spec/requests/api/files_spec.rb' - 'spec/requests/api/files_spec.rb'
- 'spec/requests/api/internal_spec.rb' - 'spec/requests/api/internal_spec.rb'
- 'spec/requests/api/issues_spec.rb' - 'spec/requests/api/issues_spec.rb'
- 'spec/requests/api/v3/issues_spec.rb'
# Offense count: 1 # Offense count: 1
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
...@@ -333,8 +332,6 @@ RSpec/ScatteredSetup: ...@@ -333,8 +332,6 @@ RSpec/ScatteredSetup:
- 'spec/lib/gitlab/bitbucket_import/importer_spec.rb' - 'spec/lib/gitlab/bitbucket_import/importer_spec.rb'
- 'spec/lib/gitlab/git/env_spec.rb' - 'spec/lib/gitlab/git/env_spec.rb'
- 'spec/requests/api/jobs_spec.rb' - 'spec/requests/api/jobs_spec.rb'
- 'spec/requests/api/v3/builds_spec.rb'
- 'spec/requests/api/v3/projects_spec.rb'
- 'spec/services/projects/create_service_spec.rb' - 'spec/services/projects/create_service_spec.rb'
# Offense count: 1 # Offense count: 1
...@@ -618,7 +615,6 @@ Style/OrAssignment: ...@@ -618,7 +615,6 @@ Style/OrAssignment:
Exclude: Exclude:
- 'app/models/concerns/token_authenticatable.rb' - 'app/models/concerns/token_authenticatable.rb'
- 'lib/api/commit_statuses.rb' - 'lib/api/commit_statuses.rb'
- 'lib/api/v3/members.rb'
- 'lib/gitlab/project_transfer.rb' - 'lib/gitlab/project_transfer.rb'
# Offense count: 50 # Offense count: 50
...@@ -781,7 +777,6 @@ Style/TernaryParentheses: ...@@ -781,7 +777,6 @@ Style/TernaryParentheses:
- 'app/finders/projects_finder.rb' - 'app/finders/projects_finder.rb'
- 'app/helpers/namespaces_helper.rb' - 'app/helpers/namespaces_helper.rb'
- 'features/support/capybara.rb' - 'features/support/capybara.rb'
- 'lib/api/v3/projects.rb'
- 'lib/gitlab/ci/build/artifacts/metadata/entry.rb' - 'lib/gitlab/ci/build/artifacts/metadata/entry.rb'
- 'spec/requests/api/pipeline_schedules_spec.rb' - 'spec/requests/api/pipeline_schedules_spec.rb'
- 'spec/support/capybara.rb' - 'spec/support/capybara.rb'
......
...@@ -2,6 +2,20 @@ ...@@ -2,6 +2,20 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 10.8.3 (2018-05-30)
### Fixed (4 changes)
- Replace Gitlab::REVISION with Gitlab.revision and handle installations without a .git directory. !19125
- Fix encoding of branch names on compare and new merge request page. !19143
- Fix remote mirror database inconsistencies when upgrading from EE to CE. !19196
- Fix local storage not being cleared after creating a new issue.
### Performance (1 change)
- Memoize Gitlab::Database.version.
## 10.8.2 (2018-05-28) ## 10.8.2 (2018-05-28)
### Security (3 changes) ### Security (3 changes)
......
...@@ -28,7 +28,7 @@ gem 'mysql2', '~> 0.4.10', group: :mysql ...@@ -28,7 +28,7 @@ gem 'mysql2', '~> 0.4.10', group: :mysql
gem 'pg', '~> 0.18.2', group: :postgres gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.27' gem 'rugged', '~> 0.27'
gem 'grape-route-helpers', '~> 2.1.0' gem 'grape-path-helpers', '~> 1.0'
gem 'faraday', '~> 0.12' gem 'faraday', '~> 0.12'
...@@ -144,6 +144,9 @@ gem 'truncato', '~> 0.7.9' ...@@ -144,6 +144,9 @@ gem 'truncato', '~> 0.7.9'
gem 'bootstrap_form', '~> 2.7.0' gem 'bootstrap_form', '~> 2.7.0'
gem 'nokogiri', '~> 1.8.2' gem 'nokogiri', '~> 1.8.2'
# Calendar rendering
gem 'icalendar'
# Diffs # Diffs
gem 'diffy', '~> 3.1.0' gem 'diffy', '~> 3.1.0'
...@@ -219,7 +222,7 @@ gem 'asana', '~> 0.6.0' ...@@ -219,7 +222,7 @@ gem 'asana', '~> 0.6.0'
gem 'ruby-fogbugz', '~> 0.2.1' gem 'ruby-fogbugz', '~> 0.2.1'
# Kubernetes integration # Kubernetes integration
gem 'kubeclient', '~> 3.0' gem 'kubeclient', '~> 3.1.0'
# Sanitize user input # Sanitize user input
gem 'sanitize', '~> 2.0' gem 'sanitize', '~> 2.0'
...@@ -320,7 +323,7 @@ group :development, :test do ...@@ -320,7 +323,7 @@ group :development, :test do
gem 'pry-byebug', '~> 3.4.1', platform: :mri gem 'pry-byebug', '~> 3.4.1', platform: :mri
gem 'pry-rails', '~> 0.3.4' gem 'pry-rails', '~> 0.3.4'
gem 'awesome_print', '~> 1.8.0', require: false gem 'awesome_print', require: false
gem 'fuubar', '~> 2.2.0' gem 'fuubar', '~> 2.2.0'
gem 'database_cleaner', '~> 1.5.0' gem 'database_cleaner', '~> 1.5.0'
......
...@@ -168,7 +168,7 @@ GEM ...@@ -168,7 +168,7 @@ GEM
diff-lcs (1.3) diff-lcs (1.3)
diffy (3.1.0) diffy (3.1.0)
docile (1.1.5) docile (1.1.5)
domain_name (0.5.20170404) domain_name (0.5.20180417)
unf (>= 0.0.5, < 1.0.0) unf (>= 0.0.5, < 1.0.0)
doorkeeper (4.3.2) doorkeeper (4.3.2)
railties (>= 4.2) railties (>= 4.2)
...@@ -348,7 +348,7 @@ GEM ...@@ -348,7 +348,7 @@ GEM
signet (~> 0.7) signet (~> 0.7)
gpgme (2.0.13) gpgme (2.0.13)
mini_portile2 (~> 2.1) mini_portile2 (~> 2.1)
grape (1.0.2) grape (1.0.3)
activesupport activesupport
builder builder
mustermann-grape (~> 1.0.0) mustermann-grape (~> 1.0.0)
...@@ -358,10 +358,10 @@ GEM ...@@ -358,10 +358,10 @@ GEM
grape-entity (0.7.1) grape-entity (0.7.1)
activesupport (>= 4.0) activesupport (>= 4.0)
multi_json (>= 1.3.2) multi_json (>= 1.3.2)
grape-route-helpers (2.1.0) grape-path-helpers (1.0.1)
activesupport activesupport (~> 4)
grape (>= 0.16.0) grape (~> 1.0)
rake rake (~> 12)
grape_logging (1.7.0) grape_logging (1.7.0)
grape grape
grpc (1.11.0) grpc (1.11.0)
...@@ -410,6 +410,7 @@ GEM ...@@ -410,6 +410,7 @@ GEM
httpclient (2.8.3) httpclient (2.8.3)
i18n (0.9.5) i18n (0.9.5)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
icalendar (2.4.1)
ice_nine (0.11.2) ice_nine (0.11.2)
influxdb (0.2.3) influxdb (0.2.3)
cause cause
...@@ -446,9 +447,9 @@ GEM ...@@ -446,9 +447,9 @@ GEM
knapsack (1.16.0) knapsack (1.16.0)
rake rake
timecop (>= 0.1.0) timecop (>= 0.1.0)
kubeclient (3.0.0) kubeclient (3.1.0)
http (~> 2.2.2) http (~> 2.2.2)
recursive-open-struct (~> 1.0.4) recursive-open-struct (~> 1.0, >= 1.0.4)
rest-client (~> 2.0) rest-client (~> 2.0)
launchy (2.4.3) launchy (2.4.3)
addressable (~> 2.3) addressable (~> 2.3)
...@@ -698,7 +699,7 @@ GEM ...@@ -698,7 +699,7 @@ GEM
re2 (1.1.1) re2 (1.1.1)
recaptcha (3.0.0) recaptcha (3.0.0)
json json
recursive-open-struct (1.0.5) recursive-open-struct (1.1.0)
redcarpet (3.4.0) redcarpet (3.4.0)
redis (3.3.5) redis (3.3.5)
redis-actionpack (5.0.2) redis-actionpack (5.0.2)
...@@ -977,7 +978,7 @@ DEPENDENCIES ...@@ -977,7 +978,7 @@ DEPENDENCIES
asciidoctor-plantuml (= 0.0.8) asciidoctor-plantuml (= 0.0.8)
asset_sync (~> 2.4) asset_sync (~> 2.4)
attr_encrypted (~> 3.1.0) attr_encrypted (~> 3.1.0)
awesome_print (~> 1.8.0) awesome_print
babosa (~> 1.0.2) babosa (~> 1.0.2)
base32 (~> 0.3.0) base32 (~> 0.3.0)
batch-loader (~> 1.2.1) batch-loader (~> 1.2.1)
...@@ -1049,7 +1050,7 @@ DEPENDENCIES ...@@ -1049,7 +1050,7 @@ DEPENDENCIES
gpgme gpgme
grape (~> 1.0) grape (~> 1.0)
grape-entity (~> 0.7.1) grape-entity (~> 0.7.1)
grape-route-helpers (~> 2.1.0) grape-path-helpers (~> 1.0)
grape_logging (~> 1.7) grape_logging (~> 1.7)
grpc (~> 1.11.0) grpc (~> 1.11.0)
haml_lint (~> 0.26.0) haml_lint (~> 0.26.0)
...@@ -1060,6 +1061,7 @@ DEPENDENCIES ...@@ -1060,6 +1061,7 @@ DEPENDENCIES
html-pipeline (~> 2.7.1) html-pipeline (~> 2.7.1)
html2text html2text
httparty (~> 0.13.3) httparty (~> 0.13.3)
icalendar
influxdb (~> 0.2) influxdb (~> 0.2)
jira-ruby (~> 1.4) jira-ruby (~> 1.4)
jquery-atwho-rails (~> 1.3.2) jquery-atwho-rails (~> 1.3.2)
...@@ -1067,7 +1069,7 @@ DEPENDENCIES ...@@ -1067,7 +1069,7 @@ DEPENDENCIES
jwt (~> 1.5.6) jwt (~> 1.5.6)
kaminari (~> 1.0) kaminari (~> 1.0)
knapsack (~> 1.16) knapsack (~> 1.16)
kubeclient (~> 3.0) kubeclient (~> 3.1.0)
letter_opener_web (~> 1.3.0) letter_opener_web (~> 1.3.0)
license_finder (~> 3.1) license_finder (~> 3.1)
licensee (~> 8.9) licensee (~> 8.9)
......
...@@ -25,8 +25,6 @@ const Api = { ...@@ -25,8 +25,6 @@ const Api = {
commitPipelinesPath: '/:project_id/commit/:sha/pipelines', commitPipelinesPath: '/:project_id/commit/:sha/pipelines',
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch', branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
createBranchPath: '/api/:version/projects/:id/repository/branches', createBranchPath: '/api/:version/projects/:id/repository/branches',
pipelinesPath: '/api/:version/projects/:id/pipelines',
pipelineJobsPath: '/api/:version/projects/:id/pipelines/:pipeline_id/jobs',
group(groupId, callback) { group(groupId, callback) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId); const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
...@@ -245,20 +243,6 @@ const Api = { ...@@ -245,20 +243,6 @@ const Api = {
}); });
}, },
pipelines(projectPath, params = {}) {
const url = Api.buildUrl(this.pipelinesPath).replace(':id', encodeURIComponent(projectPath));
return axios.get(url, { params });
},
pipelineJobs(projectPath, pipelineId, params = {}) {
const url = Api.buildUrl(this.pipelineJobsPath)
.replace(':id', encodeURIComponent(projectPath))
.replace(':pipeline_id', pipelineId);
return axios.get(url, { params });
},
buildUrl(url) { buildUrl(url) {
let urlRoot = ''; let urlRoot = '';
if (gon.relative_url_root != null) { if (gon.relative_url_root != null) {
......
/* eslint-disable comma-dangle, space-before-function-paren, one-var */ /* eslint-disable comma-dangle, space-before-function-paren, one-var */
import $ from 'jquery'; import $ from 'jquery';
import Sortable from 'vendor/Sortable'; import Sortable from 'sortablejs';
import Vue from 'vue'; import Vue from 'vue';
import AccessorUtilities from '../../lib/utils/accessor'; import AccessorUtilities from '../../lib/utils/accessor';
import boardList from './board_list.vue'; import boardList from './board_list.vue';
......
<script> <script>
import Sortable from 'vendor/Sortable'; import Sortable from 'sortablejs';
import boardNewIssue from './board_new_issue.vue'; import boardNewIssue from './board_new_issue.vue';
import boardCard from './board_card.vue'; import boardCard from './board_card.vue';
import eventHub from '../eventhub'; import eventHub from '../eventhub';
......
...@@ -41,10 +41,10 @@ gl.issueBoards.ModalEmptyState = Vue.extend({ ...@@ -41,10 +41,10 @@ gl.issueBoards.ModalEmptyState = Vue.extend({
template: ` template: `
<section class="empty-state"> <section class="empty-state">
<div class="row"> <div class="row">
<div class="col-xs-12 col-sm-6 order-sm-last"> <div class="col-12 col-md-6 order-md-last">
<aside class="svg-content"><img :src="emptyStateSvg"/></aside> <aside class="svg-content"><img :src="emptyStateSvg"/></aside>
</div> </div>
<div class="col-xs-12 col-sm-6 order-sm-first"> <div class="col-12 col-md-6 order-md-first">
<div class="text-content"> <div class="text-content">
<h4>{{ contents.title }}</h4> <h4>{{ contents.title }}</h4>
<p v-html="contents.content"></p> <p v-html="contents.content"></p>
......
/* global ListIssue */
import Vue from 'vue'; import Vue from 'vue';
import bp from '../../../breakpoints'; import bp from '../../../breakpoints';
import ModalStore from '../../stores/modal_store'; import ModalStore from '../../stores/modal_store';
...@@ -56,8 +54,11 @@ gl.issueBoards.ModalList = Vue.extend({ ...@@ -56,8 +54,11 @@ gl.issueBoards.ModalList = Vue.extend({
scrollHandler() { scrollHandler() {
const currentPage = Math.floor(this.issues.length / this.perPage); const currentPage = Math.floor(this.issues.length / this.perPage);
if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage if (
&& currentPage === this.page) { this.scrollTop() > this.scrollHeight() - 100 &&
!this.loadingNewPage &&
currentPage === this.page
) {
this.loadingNewPage = true; this.loadingNewPage = true;
this.page += 1; this.page += 1;
} }
......
<script> <script>
/* global ListIssue */ import $ from 'jquery';
import _ from 'underscore';
import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import Api from '../../api';
import $ from 'jquery'; export default {
import _ from 'underscore'; name: 'BoardProjectSelect',
import eventHub from '../eventhub'; components: {
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; loadingIcon,
import Api from '../../api'; },
props: {
export default { groupId: {
name: 'BoardProjectSelect', type: Number,
components: { required: true,
loadingIcon, default: 0,
},
props: {
groupId: {
type: Number,
required: true,
default: 0,
},
}, },
data() { },
return { data() {
loading: true, return {
selectedProject: {}, loading: true,
}; selectedProject: {},
};
},
computed: {
selectedProjectName() {
return this.selectedProject.name || 'Select a project';
}, },
computed: { },
selectedProjectName() { mounted() {
return this.selectedProject.name || 'Select a project'; $(this.$refs.projectsDropdown).glDropdown({
filterable: true,
filterRemote: true,
search: {
fields: ['name_with_namespace'],
}, },
}, clicked: ({ $el, e }) => {
mounted() { e.preventDefault();
$(this.$refs.projectsDropdown).glDropdown({ this.selectedProject = {
filterable: true, id: $el.data('project-id'),
filterRemote: true, name: $el.data('project-name'),
search: { };
fields: ['name_with_namespace'], eventHub.$emit('setSelectedProject', this.selectedProject);
}, },
clicked: ({ $el, e }) => { selectable: true,
e.preventDefault(); data: (term, callback) => {
this.selectedProject = { this.loading = true;
id: $el.data('project-id'), return Api.groupProjects(this.groupId, term, projects => {
name: $el.data('project-name'), this.loading = false;
}; callback(projects);
eventHub.$emit('setSelectedProject', this.selectedProject); });
}, },
selectable: true, renderRow(project) {
data: (term, callback) => { return `
this.loading = true;
return Api.groupProjects(this.groupId, term, (projects) => {
this.loading = false;
callback(projects);
});
},
renderRow(project) {
return `
<li> <li>
<a href='#' class='dropdown-menu-link' data-project-id="${project.id}" data-project-name="${project.name}"> <a href='#' class='dropdown-menu-link' data-project-id="${project.id}" data-project-name="${project.name}">
${_.escape(project.name)} ${_.escape(project.name)}
</a> </a>
</li> </li>
`; `;
}, },
text: project => project.name, text: project => project.name,
}); });
}, },
}; };
</script> </script>
<template> <template>
......
...@@ -31,6 +31,7 @@ export default class Clusters { ...@@ -31,6 +31,7 @@ export default class Clusters {
installHelmPath, installHelmPath,
installIngressPath, installIngressPath,
installRunnerPath, installRunnerPath,
installJupyterPath,
installPrometheusPath, installPrometheusPath,
managePrometheusPath, managePrometheusPath,
clusterStatus, clusterStatus,
...@@ -51,6 +52,7 @@ export default class Clusters { ...@@ -51,6 +52,7 @@ export default class Clusters {
installIngressEndpoint: installIngressPath, installIngressEndpoint: installIngressPath,
installRunnerEndpoint: installRunnerPath, installRunnerEndpoint: installRunnerPath,
installPrometheusEndpoint: installPrometheusPath, installPrometheusEndpoint: installPrometheusPath,
installJupyterEndpoint: installJupyterPath,
}); });
this.installApplication = this.installApplication.bind(this); this.installApplication = this.installApplication.bind(this);
...@@ -209,11 +211,12 @@ export default class Clusters { ...@@ -209,11 +211,12 @@ export default class Clusters {
} }
} }
installApplication(appId) { installApplication(data) {
const appId = data.id;
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING); this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING);
this.store.updateAppProperty(appId, 'requestReason', null); this.store.updateAppProperty(appId, 'requestReason', null);
this.service.installApplication(appId) this.service.installApplication(appId, data.params)
.then(() => { .then(() => {
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS); this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS);
}) })
......
...@@ -52,6 +52,11 @@ ...@@ -52,6 +52,11 @@
type: String, type: String,
required: false, required: false,
}, },
installApplicationRequestParams: {
type: Object,
required: false,
default: () => ({}),
},
}, },
computed: { computed: {
rowJsClass() { rowJsClass() {
...@@ -109,7 +114,10 @@ ...@@ -109,7 +114,10 @@
}, },
methods: { methods: {
installClicked() { installClicked() {
eventHub.$emit('installApplication', this.id); eventHub.$emit('installApplication', {
id: this.id,
params: this.installApplicationRequestParams,
});
}, },
}, },
}; };
......
...@@ -121,6 +121,12 @@ export default { ...@@ -121,6 +121,12 @@ export default {
false, false,
); );
}, },
jupyterInstalled() {
return this.applications.jupyter.status === APPLICATION_INSTALLED;
},
jupyterHostname() {
return this.applications.jupyter.hostname;
},
}, },
}; };
</script> </script>
...@@ -278,11 +284,67 @@ export default { ...@@ -278,11 +284,67 @@ export default {
applications to production.`) }} applications to production.`) }}
</div> </div>
</application-row> </application-row>
<application-row
id="jupyter"
:title="applications.jupyter.title"
title-link="https://jupyterhub.readthedocs.io/en/stable/"
:status="applications.jupyter.status"
:status-reason="applications.jupyter.statusReason"
:request-status="applications.jupyter.requestStatus"
:request-reason="applications.jupyter.requestReason"
:install-application-request-params="{ hostname: applications.jupyter.hostname }"
>
<div slot="description">
<p>
{{ s__(`ClusterIntegration|JupyterHub, a multi-user Hub, spawns,
manages, and proxies multiple instances of the single-user
Jupyter notebook server. JupyterHub can be used to serve
notebooks to a class of students, a corporate data science group,
or a scientific research group.`) }}
</p>
<template v-if="ingressExternalIp">
<div class="form-group">
<label for="jupyter-hostname">
{{ s__('ClusterIntegration|Jupyter Hostname') }}
</label>
<div class="input-group">
<input
type="text"
class="form-control js-hostname"
v-model="applications.jupyter.hostname"
:readonly="jupyterInstalled"
/>
<span
class="input-group-btn"
>
<clipboard-button
:text="jupyterHostname"
:title="s__('ClusterIntegration|Copy Jupyter Hostname to clipboard')"
class="js-clipboard-btn"
/>
</span>
</div>
</div>
<p v-if="ingressInstalled">
{{ s__(`ClusterIntegration|Replace this with your own hostname if you want.
If you do so, point hostname to Ingress IP Address from above.`) }}
<a
:href="ingressDnsHelpPath"
target="_blank"
rel="noopener noreferrer"
>
{{ __('More information') }}
</a>
</p>
</template>
</div>
</application-row>
<!-- <!--
NOTE: Don't forget to update `clusters.scss` NOTE: Don't forget to update `clusters.scss`
min-height for this block and uncomment `application_spec` tests min-height for this block and uncomment `application_spec` tests
--> -->
<!-- Add GitLab Runner row, all other plumbing is complete -->
</div> </div>
</div> </div>
</section> </section>
......
...@@ -11,3 +11,4 @@ export const REQUEST_LOADING = 'request-loading'; ...@@ -11,3 +11,4 @@ export const REQUEST_LOADING = 'request-loading';
export const REQUEST_SUCCESS = 'request-success'; export const REQUEST_SUCCESS = 'request-success';
export const REQUEST_FAILURE = 'request-failure'; export const REQUEST_FAILURE = 'request-failure';
export const INGRESS = 'ingress'; export const INGRESS = 'ingress';
export const JUPYTER = 'jupyter';
...@@ -8,6 +8,7 @@ export default class ClusterService { ...@@ -8,6 +8,7 @@ export default class ClusterService {
ingress: this.options.installIngressEndpoint, ingress: this.options.installIngressEndpoint,
runner: this.options.installRunnerEndpoint, runner: this.options.installRunnerEndpoint,
prometheus: this.options.installPrometheusEndpoint, prometheus: this.options.installPrometheusEndpoint,
jupyter: this.options.installJupyterEndpoint,
}; };
} }
...@@ -15,8 +16,8 @@ export default class ClusterService { ...@@ -15,8 +16,8 @@ export default class ClusterService {
return axios.get(this.options.endpoint); return axios.get(this.options.endpoint);
} }
installApplication(appId) { installApplication(appId, params) {
return axios.post(this.appInstallEndpointMap[appId]); return axios.post(this.appInstallEndpointMap[appId], params);
} }
static updateCluster(endpoint, data) { static updateCluster(endpoint, data) {
......
import { s__ } from '../../locale'; import { s__ } from '../../locale';
import { INGRESS } from '../constants'; import { INGRESS, JUPYTER } from '../constants';
export default class ClusterStore { export default class ClusterStore {
constructor() { constructor() {
...@@ -38,6 +38,14 @@ export default class ClusterStore { ...@@ -38,6 +38,14 @@ export default class ClusterStore {
requestStatus: null, requestStatus: null,
requestReason: null, requestReason: null,
}, },
jupyter: {
title: s__('ClusterIntegration|JupyterHub'),
status: null,
statusReason: null,
requestStatus: null,
requestReason: null,
hostname: null,
},
}, },
}; };
} }
...@@ -83,6 +91,12 @@ export default class ClusterStore { ...@@ -83,6 +91,12 @@ export default class ClusterStore {
if (appId === INGRESS) { if (appId === INGRESS) {
this.state.applications.ingress.externalIp = serverAppEntry.external_ip; this.state.applications.ingress.externalIp = serverAppEntry.external_ip;
} else if (appId === JUPYTER) {
this.state.applications.jupyter.hostname =
serverAppEntry.hostname ||
(this.state.applications.ingress.externalIp
? `jupyter.${this.state.applications.ingress.externalIp}.xip.io`
: '');
} }
}); });
} }
......
...@@ -126,7 +126,6 @@ export default { ...@@ -126,7 +126,6 @@ export default {
</div> </div>
<form <form
v-if="!isCompact" v-if="!isCompact"
class="form-horizontal"
@submit.prevent.stop="commitChanges" @submit.prevent.stop="commitChanges"
ref="formEl" ref="formEl"
> >
......
<script>
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
Icon,
},
props: {
file: {
type: Object,
required: true,
},
},
computed: {
showButtons() {
return this.file.permalink;
},
},
};
</script>
<template>
<div
v-if="showButtons"
class="pull-right ide-btn-group"
>
<a
:href="file.permalink"
target="_blank"
:title="s__('IDE|Open in file view')"
rel="noopener noreferrer"
>
<span class="vertical-align-middle">Open in file view</span>
<icon
name="external-link"
css-classes="vertical-align-middle space-right"
:size="16"
/>
</a>
</div>
</template>
...@@ -6,6 +6,7 @@ import RepoTabs from './repo_tabs.vue'; ...@@ -6,6 +6,7 @@ import RepoTabs from './repo_tabs.vue';
import IdeStatusBar from './ide_status_bar.vue'; import IdeStatusBar from './ide_status_bar.vue';
import RepoEditor from './repo_editor.vue'; import RepoEditor from './repo_editor.vue';
import FindFile from './file_finder/index.vue'; import FindFile from './file_finder/index.vue';
import RightPane from './panes/right.vue';
const originalStopCallback = Mousetrap.stopCallback; const originalStopCallback = Mousetrap.stopCallback;
...@@ -16,6 +17,7 @@ export default { ...@@ -16,6 +17,7 @@ export default {
IdeStatusBar, IdeStatusBar,
RepoEditor, RepoEditor,
FindFile, FindFile,
RightPane,
}, },
computed: { computed: {
...mapState([ ...mapState([
...@@ -25,6 +27,7 @@ export default { ...@@ -25,6 +27,7 @@ export default {
'currentMergeRequestId', 'currentMergeRequestId',
'fileFindVisible', 'fileFindVisible',
'emptyStateSvgPath', 'emptyStateSvgPath',
'currentProjectId',
]), ]),
...mapGetters(['activeFile', 'hasChanges']), ...mapGetters(['activeFile', 'hasChanges']),
}, },
...@@ -122,6 +125,9 @@ export default { ...@@ -122,6 +125,9 @@ export default {
</div> </div>
</template> </template>
</div> </div>
<right-pane
v-if="currentProjectId"
/>
</div> </div>
<ide-status-bar :file="activeFile"/> <ide-status-bar :file="activeFile"/>
</article> </article>
......
<script>
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
export default {
components: {
Icon,
},
directives: {
tooltip,
},
props: {
file: {
type: Object,
required: true,
},
},
computed: {
showButtons() {
return (
this.file.rawPath || this.file.blamePath || this.file.commitsPath || this.file.permalink
);
},
rawDownloadButtonLabel() {
return this.file.binary ? __('Download') : __('Raw');
},
},
};
</script>
<template>
<div
v-if="showButtons"
class="float-right ide-btn-group"
>
<a
v-tooltip
v-if="!file.binary"
:href="file.blamePath"
:title="__('Blame')"
class="btn btn-sm btn-transparent blame"
>
<icon
name="blame"
:size="16"
/>
</a>
<a
v-tooltip
:href="file.commitsPath"
:title="__('History')"
class="btn btn-sm btn-transparent history"
>
<icon
name="history"
:size="16"
/>
</a>
<a
v-tooltip
:href="file.permalink"
:title="__('Permalink')"
class="btn btn-sm btn-transparent permalink"
>
<icon
name="link"
:size="16"
/>
</a>
<a
v-tooltip
:href="file.rawPath"
target="_blank"
class="btn btn-sm btn-transparent prepend-left-10 raw"
rel="noopener noreferrer"
:title="rawDownloadButtonLabel">
<icon
name="download"
:size="16"
/>
</a>
</div>
</template>
...@@ -31,6 +31,7 @@ export default { ...@@ -31,6 +31,7 @@ export default {
computed: { computed: {
...mapState(['currentBranchId', 'currentProjectId']), ...mapState(['currentBranchId', 'currentProjectId']),
...mapGetters(['currentProject', 'lastCommit']), ...mapGetters(['currentProject', 'lastCommit']),
...mapState('pipelines', ['latestPipeline']),
}, },
watch: { watch: {
lastCommit() { lastCommit() {
...@@ -51,14 +52,14 @@ export default { ...@@ -51,14 +52,14 @@ export default {
} }
}, },
methods: { methods: {
...mapActions(['pipelinePoll', 'stopPipelinePolling']), ...mapActions('pipelines', ['fetchLatestPipeline', 'stopPipelinePolling']),
startTimer() { startTimer() {
this.intervalId = setInterval(() => { this.intervalId = setInterval(() => {
this.commitAgeUpdate(); this.commitAgeUpdate();
}, 1000); }, 1000);
}, },
initPipelinePolling() { initPipelinePolling() {
this.pipelinePoll(); this.fetchLatestPipeline();
this.isPollingInitialized = true; this.isPollingInitialized = true;
}, },
commitAgeUpdate() { commitAgeUpdate() {
...@@ -81,18 +82,18 @@ export default { ...@@ -81,18 +82,18 @@ export default {
> >
<span <span
class="ide-status-pipeline" class="ide-status-pipeline"
v-if="lastCommit.pipeline && lastCommit.pipeline.details" v-if="latestPipeline && latestPipeline.details"
> >
<ci-icon <ci-icon
:status="lastCommit.pipeline.details.status" :status="latestPipeline.details.status"
v-tooltip v-tooltip
:title="lastCommit.pipeline.details.status.text" :title="latestPipeline.details.status.text"
/> />
Pipeline Pipeline
<a <a
class="monospace" class="monospace"
:href="lastCommit.pipeline.details.status.details_path">#{{ lastCommit.pipeline.id }}</a> :href="latestPipeline.details.status.details_path">#{{ latestPipeline.id }}</a>
{{ lastCommit.pipeline.details.status.text }} {{ latestPipeline.details.status.text }}
for for
</span> </span>
......
<script>
import Icon from '../../../vue_shared/components/icon.vue';
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
export default {
components: {
Icon,
CiIcon,
},
props: {
job: {
type: Object,
required: true,
},
},
computed: {
jobId() {
return `#${this.job.id}`;
},
},
};
</script>
<template>
<div class="ide-job-item">
<ci-icon
:status="job.status"
:borderless="true"
:size="24"
/>
<span class="prepend-left-8">
{{ job.name }}
<a
:href="job.path"
target="_blank"
class="ide-external-link"
>
{{ jobId }}
<icon
name="external-link"
:size="12"
/>
</a>
</span>
</div>
</template>
<script>
import { mapActions } from 'vuex';
import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
import Stage from './stage.vue';
export default {
components: {
LoadingIcon,
Stage,
},
props: {
stages: {
type: Array,
required: true,
},
loading: {
type: Boolean,
required: true,
},
},
methods: {
...mapActions('pipelines', ['fetchJobs', 'toggleStageCollapsed']),
},
};
</script>
<template>
<div>
<loading-icon
v-if="loading && !stages.length"
class="prepend-top-default"
size="2"
/>
<template v-else>
<stage
v-for="stage in stages"
:key="stage.id"
:stage="stage"
@fetch="fetchJobs"
@toggleCollapsed="toggleStageCollapsed"
/>
</template>
</div>
</template>
<script>
import tooltip from '../../../vue_shared/directives/tooltip';
import Icon from '../../../vue_shared/components/icon.vue';
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
import Item from './item.vue';
export default {
directives: {
tooltip,
},
components: {
Icon,
CiIcon,
LoadingIcon,
Item,
},
props: {
stage: {
type: Object,
required: true,
},
},
data() {
return {
showTooltip: false,
};
},
computed: {
collapseIcon() {
return this.stage.isCollapsed ? 'angle-left' : 'angle-down';
},
showLoadingIcon() {
return this.stage.isLoading && !this.stage.jobs.length;
},
jobsCount() {
return this.stage.jobs.length;
},
},
mounted() {
const { stageTitle } = this.$refs;
this.showTooltip = stageTitle.scrollWidth > stageTitle.offsetWidth;
this.$emit('fetch', this.stage);
},
methods: {
toggleCollapsed() {
this.$emit('toggleCollapsed', this.stage.id);
},
},
};
</script>
<template>
<div
class="ide-stage card prepend-top-default"
>
<div
class="card-header"
:class="{
'border-bottom-0': stage.isCollapsed
}"
@click="toggleCollapsed"
>
<ci-icon
:status="stage.status"
:size="24"
/>
<strong
v-tooltip="showTooltip"
:title="showTooltip ? stage.name : null"
data-container="body"
class="prepend-left-8 ide-stage-title"
ref="stageTitle"
>
{{ stage.name }}
</strong>
<div
v-if="!stage.isLoading || stage.jobs.length"
class="append-right-8 prepend-left-4"
>
<span class="badge badge-pill">
{{ jobsCount }}
</span>
</div>
<icon
:name="collapseIcon"
css-classes="ide-stage-collapse-icon"
/>
</div>
<div
class="card-body"
v-show="!stage.isCollapsed"
>
<loading-icon
v-if="showLoadingIcon"
/>
<template v-else>
<item
v-for="job in stage.jobs"
:key="job.id"
:job="job"
/>
</template>
</div>
</div>
</template>
<script>
import { mapActions, mapState } from 'vuex';
import tooltip from '../../../vue_shared/directives/tooltip';
import Icon from '../../../vue_shared/components/icon.vue';
import { rightSidebarViews } from '../../constants';
import PipelinesList from '../pipelines/list.vue';
export default {
directives: {
tooltip,
},
components: {
Icon,
PipelinesList,
},
computed: {
...mapState(['rightPane']),
},
methods: {
...mapActions(['setRightPane']),
clickTab(e, view) {
e.target.blur();
this.setRightPane(view);
},
},
rightSidebarViews,
};
</script>
<template>
<div
class="multi-file-commit-panel ide-right-sidebar"
>
<div
class="multi-file-commit-panel-inner"
v-if="rightPane"
>
<component :is="rightPane" />
</div>
<nav class="ide-activity-bar">
<ul class="list-unstyled">
<li>
<button
v-tooltip
data-container="body"
data-placement="left"
:title="__('Pipelines')"
class="ide-sidebar-link is-right"
:class="{
active: rightPane === $options.rightSidebarViews.pipelines
}"
type="button"
@click="clickTab($event, $options.rightSidebarViews.pipelines)"
>
<icon
:size="16"
name="pipeline"
/>
</button>
</li>
</ul>
</nav>
</div>
</template>
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
import { sprintf, __ } from '../../../locale';
import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
import Icon from '../../../vue_shared/components/icon.vue';
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
import Tabs from '../../../vue_shared/components/tabs/tabs';
import Tab from '../../../vue_shared/components/tabs/tab.vue';
import EmptyState from '../../../pipelines/components/empty_state.vue';
import JobsList from '../jobs/list.vue';
export default {
components: {
LoadingIcon,
Icon,
CiIcon,
Tabs,
Tab,
JobsList,
EmptyState,
},
computed: {
...mapState(['pipelinesEmptyStateSvgPath', 'links']),
...mapGetters(['currentProject']),
...mapGetters('pipelines', ['jobsCount', 'failedJobsCount', 'failedStages', 'pipelineFailed']),
...mapState('pipelines', ['isLoadingPipeline', 'latestPipeline', 'stages', 'isLoadingJobs']),
ciLintText() {
return sprintf(
__('You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}'),
{
linkStart: `<a href="${_.escape(this.currentProject.web_url)}/-/ci/lint">`,
linkEnd: '</a>',
},
false,
);
},
showLoadingIcon() {
return this.isLoadingPipeline && this.latestPipeline === null;
},
},
created() {
this.fetchLatestPipeline();
},
methods: {
...mapActions('pipelines', ['fetchLatestPipeline']),
},
};
</script>
<template>
<div class="ide-pipeline">
<loading-icon
v-if="showLoadingIcon"
class="prepend-top-default"
size="2"
/>
<template v-else-if="latestPipeline !== null">
<header
v-if="latestPipeline"
class="ide-tree-header ide-pipeline-header"
>
<ci-icon
:status="latestPipeline.details.status"
:size="24"
/>
<span class="prepend-left-8">
<strong>
{{ __('Pipeline') }}
</strong>
<a
:href="latestPipeline.path"
target="_blank"
class="ide-external-link"
>
#{{ latestPipeline.id }}
<icon
name="external-link"
:size="12"
/>
</a>
</span>
</header>
<empty-state
v-if="latestPipeline === false"
:help-page-path="links.ciHelpPagePath"
:empty-state-svg-path="pipelinesEmptyStateSvgPath"
:can-set-ci="true"
/>
<div
v-else-if="latestPipeline.yamlError"
class="bs-callout bs-callout-danger"
>
<p class="append-bottom-0">
{{ __('Found errors in your .gitlab-ci.yml:') }}
</p>
<p class="append-bottom-0">
{{ latestPipeline.yamlError }}
</p>
<p
class="append-bottom-0"
v-html="ciLintText"
></p>
</div>
<tabs
v-else
class="ide-pipeline-list"
>
<tab
:active="!pipelineFailed"
>
<template slot="title">
{{ __('Jobs') }}
<span
v-if="jobsCount"
class="badge badge-pill"
>
{{ jobsCount }}
</span>
</template>
<jobs-list
:loading="isLoadingJobs"
:stages="stages"
/>
</tab>
<tab
:active="pipelineFailed"
>
<template slot="title">
{{ __('Failed Jobs') }}
<span
v-if="failedJobsCount"
class="badge badge-pill"
>
{{ failedJobsCount }}
</span>
</template>
<jobs-list
:loading="isLoadingJobs"
:stages="failedStages"
/>
</tab>
</tabs>
</template>
</div>
</template>
...@@ -6,12 +6,12 @@ import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer ...@@ -6,12 +6,12 @@ import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer
import { activityBarViews, viewerTypes } from '../constants'; import { activityBarViews, viewerTypes } from '../constants';
import monacoLoader from '../monaco_loader'; import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor'; import Editor from '../lib/editor';
import IdeFileButtons from './ide_file_buttons.vue'; import ExternalLink from './external_link.vue';
export default { export default {
components: { components: {
ContentViewer, ContentViewer,
IdeFileButtons, ExternalLink,
}, },
props: { props: {
file: { file: {
...@@ -224,7 +224,7 @@ export default { ...@@ -224,7 +224,7 @@ export default {
</a> </a>
</li> </li>
</ul> </ul>
<ide-file-buttons <external-link
:file="file" :file="file"
/> />
</div> </div>
......
...@@ -20,3 +20,7 @@ export const viewerTypes = { ...@@ -20,3 +20,7 @@ export const viewerTypes = {
edit: 'editor', edit: 'editor',
diff: 'diff', diff: 'diff',
}; };
export const rightSidebarViews = {
pipelines: 'pipelines-list',
};
...@@ -63,7 +63,7 @@ router.beforeEach((to, from, next) => { ...@@ -63,7 +63,7 @@ router.beforeEach((to, from, next) => {
.then(() => { .then(() => {
const fullProjectId = `${to.params.namespace}/${to.params.project}`; const fullProjectId = `${to.params.namespace}/${to.params.project}`;
const baseSplit = to.params[0].split('/-/'); const baseSplit = (to.params[0] && to.params[0].split('/-/')) || [''];
const branchId = baseSplit[0].slice(-1) === '/' ? baseSplit[0].slice(0, -1) : baseSplit[0]; const branchId = baseSplit[0].slice(-1) === '/' ? baseSplit[0].slice(0, -1) : baseSplit[0];
if (branchId) { if (branchId) {
......
import Vue from 'vue'; import Vue from 'vue';
import { mapActions } from 'vuex';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import ide from './components/ide.vue'; import ide from './components/ide.vue';
import store from './stores'; import store from './stores';
...@@ -17,11 +18,18 @@ export function initIde(el) { ...@@ -17,11 +18,18 @@ export function initIde(el) {
ide, ide,
}, },
created() { created() {
this.$store.dispatch('setEmptyStateSvgs', { this.setEmptyStateSvgs({
emptyStateSvgPath: el.dataset.emptyStateSvgPath, emptyStateSvgPath: el.dataset.emptyStateSvgPath,
noChangesStateSvgPath: el.dataset.noChangesStateSvgPath, noChangesStateSvgPath: el.dataset.noChangesStateSvgPath,
committedStateSvgPath: el.dataset.committedStateSvgPath, committedStateSvgPath: el.dataset.committedStateSvgPath,
pipelinesEmptyStateSvgPath: el.dataset.pipelinesEmptyStateSvgPath,
}); });
this.setLinks({
ciHelpPagePath: el.dataset.ciHelpPagePath,
});
},
methods: {
...mapActions(['setEmptyStateSvgs', 'setLinks']),
}, },
render(createElement) { render(createElement) {
return createElement('ide'); return createElement('ide');
......
/* global monaco */
import Disposable from './disposable'; import Disposable from './disposable';
import eventHub from '../../eventhub'; import eventHub from '../../eventhub';
......
...@@ -169,6 +169,12 @@ export const burstUnusedSeal = ({ state, commit }) => { ...@@ -169,6 +169,12 @@ export const burstUnusedSeal = ({ state, commit }) => {
} }
}; };
export const setRightPane = ({ commit }, view) => {
commit(types.SET_RIGHT_PANE, view);
};
export const setLinks = ({ commit }, links) => commit(types.SET_LINKS, links);
export * from './actions/tree'; export * from './actions/tree';
export * from './actions/file'; export * from './actions/file';
export * from './actions/project'; export * from './actions/project';
......
...@@ -84,11 +84,11 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive ...@@ -84,11 +84,11 @@ export const getFileData = ({ state, commit, dispatch }, { path, makeFileActive
}); });
}; };
export const setFileMrChange = ({ state, commit }, { file, mrChange }) => { export const setFileMrChange = ({ commit }, { file, mrChange }) => {
commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange }); commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange });
}; };
export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => { export const getRawFileData = ({ state, commit }, { path, baseSha }) => {
const file = state.entries[path]; const file = state.entries[path];
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
service service
...@@ -156,7 +156,7 @@ export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn ...@@ -156,7 +156,7 @@ export const setEditorPosition = ({ getters, commit }, { editorRow, editorColumn
} }
}; };
export const setFileViewMode = ({ state, commit }, { file, viewMode }) => { export const setFileViewMode = ({ commit }, { file, viewMode }) => {
commit(types.SET_FILE_VIEWMODE, { file, viewMode }); commit(types.SET_FILE_VIEWMODE, { file, viewMode });
}; };
......
...@@ -3,7 +3,7 @@ import service from '../../services'; ...@@ -3,7 +3,7 @@ import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
export const getMergeRequestData = ( export const getMergeRequestData = (
{ commit, state, dispatch }, { commit, state },
{ projectId, mergeRequestId, force = false } = {}, { projectId, mergeRequestId, force = false } = {},
) => ) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
...@@ -32,7 +32,7 @@ export const getMergeRequestData = ( ...@@ -32,7 +32,7 @@ export const getMergeRequestData = (
}); });
export const getMergeRequestChanges = ( export const getMergeRequestChanges = (
{ commit, state, dispatch }, { commit, state },
{ projectId, mergeRequestId, force = false } = {}, { projectId, mergeRequestId, force = false } = {},
) => ) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
...@@ -58,7 +58,7 @@ export const getMergeRequestChanges = ( ...@@ -58,7 +58,7 @@ export const getMergeRequestChanges = (
}); });
export const getMergeRequestVersions = ( export const getMergeRequestVersions = (
{ commit, state, dispatch }, { commit, state },
{ projectId, mergeRequestId, force = false } = {}, { projectId, mergeRequestId, force = false } = {},
) => ) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
......
import Visibility from 'visibilityjs';
import flash from '~/flash'; import flash from '~/flash';
import { __ } from '~/locale'; import { __ } from '~/locale';
import service from '../../services'; import service from '../../services';
import * as types from '../mutation_types'; import * as types from '../mutation_types';
import Poll from '../../../lib/utils/poll';
let eTagPoll; export const getProjectData = ({ commit, state }, { namespace, projectId, force = false } = {}) =>
export const getProjectData = (
{ commit, state, dispatch },
{ namespace, projectId, force = false } = {},
) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
if (!state.projects[`${namespace}/${projectId}`] || force) { if (!state.projects[`${namespace}/${projectId}`] || force) {
commit(types.TOGGLE_LOADING, { entry: state }); commit(types.TOGGLE_LOADING, { entry: state });
...@@ -40,10 +33,7 @@ export const getProjectData = ( ...@@ -40,10 +33,7 @@ export const getProjectData = (
} }
}); });
export const getBranchData = ( export const getBranchData = ({ commit, state }, { projectId, branchId, force = false } = {}) =>
{ commit, state, dispatch },
{ projectId, branchId, force = false } = {},
) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
if ( if (
typeof state.projects[`${projectId}`] === 'undefined' || typeof state.projects[`${projectId}`] === 'undefined' ||
...@@ -78,7 +68,7 @@ export const getBranchData = ( ...@@ -78,7 +68,7 @@ export const getBranchData = (
} }
}); });
export const refreshLastCommitData = ({ commit, state, dispatch }, { projectId, branchId } = {}) => export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {}) =>
service service
.getBranchData(projectId, branchId) .getBranchData(projectId, branchId)
.then(({ data }) => { .then(({ data }) => {
...@@ -91,61 +81,3 @@ export const refreshLastCommitData = ({ commit, state, dispatch }, { projectId, ...@@ -91,61 +81,3 @@ export const refreshLastCommitData = ({ commit, state, dispatch }, { projectId,
.catch(() => { .catch(() => {
flash(__('Error loading last commit.'), 'alert', document, null, false, true); flash(__('Error loading last commit.'), 'alert', document, null, false, true);
}); });
export const pollSuccessCallBack = ({ commit, state, dispatch }, { data }) => {
if (data.pipelines && data.pipelines.length) {
const lastCommitHash =
state.projects[state.currentProjectId].branches[state.currentBranchId].commit.id;
const lastCommitPipeline = data.pipelines.find(
pipeline => pipeline.commit.id === lastCommitHash,
);
commit(types.SET_LAST_COMMIT_PIPELINE, {
projectId: state.currentProjectId,
branchId: state.currentBranchId,
pipeline: lastCommitPipeline || {},
});
}
return data;
};
export const pipelinePoll = ({ getters, dispatch }) => {
eTagPoll = new Poll({
resource: service,
method: 'lastCommitPipelines',
data: {
getters,
},
successCallback: ({ data }) => dispatch('pollSuccessCallBack', { data }),
errorCallback: () => {
flash(
__('Something went wrong while fetching the latest pipeline status.'),
'alert',
document,
null,
false,
true,
);
},
});
if (!Visibility.hidden()) {
eTagPoll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
eTagPoll.restart();
} else {
eTagPoll.stop();
}
});
};
export const stopPipelinePolling = () => {
eTagPoll.stop();
};
export const restartPipelinePolling = () => {
eTagPoll.restart();
};
...@@ -5,7 +5,7 @@ import * as types from '../mutation_types'; ...@@ -5,7 +5,7 @@ import * as types from '../mutation_types';
import { findEntry } from '../utils'; import { findEntry } from '../utils';
import FilesDecoratorWorker from '../workers/files_decorator_worker'; import FilesDecoratorWorker from '../workers/files_decorator_worker';
export const toggleTreeOpen = ({ commit, dispatch }, path) => { export const toggleTreeOpen = ({ commit }, path) => {
commit(types.TOGGLE_TREE_OPEN, path); commit(types.TOGGLE_TREE_OPEN, path);
}; };
...@@ -23,7 +23,7 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => { ...@@ -23,7 +23,7 @@ export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
} }
}; };
export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => { export const getLastCommitData = ({ state, commit, dispatch }, tree = state) => {
if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return; if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
service service
...@@ -49,7 +49,7 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s ...@@ -49,7 +49,7 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
.catch(() => flash('Error fetching log data.', 'alert', document, null, false, true)); .catch(() => flash('Error fetching log data.', 'alert', document, null, false, true));
}; };
export const getFiles = ({ state, commit, dispatch }, { projectId, branchId } = {}) => export const getFiles = ({ state, commit }, { projectId, branchId } = {}) =>
new Promise((resolve, reject) => { new Promise((resolve, reject) => {
if (!state.trees[`${projectId}/${branchId}`]) { if (!state.trees[`${projectId}/${branchId}`]) {
const selectedProject = state.projects[projectId]; const selectedProject = state.projects[projectId];
......
...@@ -10,14 +10,17 @@ import mergeRequests from './modules/merge_requests'; ...@@ -10,14 +10,17 @@ import mergeRequests from './modules/merge_requests';
Vue.use(Vuex); Vue.use(Vuex);
export default new Vuex.Store({ export const createStore = () =>
state: state(), new Vuex.Store({
actions, state: state(),
mutations, actions,
getters, mutations,
modules: { getters,
commit: commitModule, modules: {
pipelines, commit: commitModule,
mergeRequests, pipelines,
}, mergeRequests,
}); },
});
export default createStore();
...@@ -31,9 +31,9 @@ export const setLastCommitMessage = ({ rootState, commit }, data) => { ...@@ -31,9 +31,9 @@ export const setLastCommitMessage = ({ rootState, commit }, data) => {
const currentProject = rootState.projects[rootState.currentProjectId]; const currentProject = rootState.projects[rootState.currentProjectId];
const commitStats = data.stats const commitStats = data.stats
? sprintf(__('with %{additions} additions, %{deletions} deletions.'), { ? sprintf(__('with %{additions} additions, %{deletions} deletions.'), {
additions: data.stats.additions, // eslint-disable-line indent additions: data.stats.additions, // eslint-disable-line indent-legacy
deletions: data.stats.deletions, // eslint-disable-line indent deletions: data.stats.deletions, // eslint-disable-line indent-legacy
}) // eslint-disable-line indent }) // eslint-disable-line indent-legacy
: ''; : '';
const commitMsg = sprintf( const commitMsg = sprintf(
__('Your changes have been committed. Commit %{commitId} %{commitStats}'), __('Your changes have been committed. Commit %{commitId} %{commitStats}'),
...@@ -74,10 +74,7 @@ export const checkCommitStatus = ({ rootState }) => ...@@ -74,10 +74,7 @@ export const checkCommitStatus = ({ rootState }) =>
), ),
); );
export const updateFilesAfterCommit = ( export const updateFilesAfterCommit = ({ commit, dispatch, rootState }, { data }) => {
{ commit, dispatch, state, rootState, rootGetters },
{ data },
) => {
const selectedProject = rootState.projects[rootState.currentProjectId]; const selectedProject = rootState.projects[rootState.currentProjectId];
const lastCommit = { const lastCommit = {
commit_path: `${selectedProject.web_url}/commit/${data.id}`, commit_path: `${selectedProject.web_url}/commit/${data.id}`,
......
import Visibility from 'visibilityjs';
import axios from 'axios';
import { __ } from '../../../../locale'; import { __ } from '../../../../locale';
import Api from '../../../../api';
import flash from '../../../../flash'; import flash from '../../../../flash';
import Poll from '../../../../lib/utils/poll';
import service from '../../../services';
import * as types from './mutation_types'; import * as types from './mutation_types';
let eTagPoll;
export const clearEtagPoll = () => {
eTagPoll = null;
};
export const stopPipelinePolling = () => eTagPoll && eTagPoll.stop();
export const restartPipelinePolling = () => eTagPoll && eTagPoll.restart();
export const requestLatestPipeline = ({ commit }) => commit(types.REQUEST_LATEST_PIPELINE); export const requestLatestPipeline = ({ commit }) => commit(types.REQUEST_LATEST_PIPELINE);
export const receiveLatestPipelineError = ({ commit }) => { export const receiveLatestPipelineError = ({ commit, dispatch }) => {
flash(__('There was an error loading latest pipeline')); flash(__('There was an error loading latest pipeline'));
commit(types.RECEIVE_LASTEST_PIPELINE_ERROR); commit(types.RECEIVE_LASTEST_PIPELINE_ERROR);
dispatch('stopPipelinePolling');
};
export const receiveLatestPipelineSuccess = ({ rootGetters, commit }, { pipelines }) => {
let lastCommitPipeline = false;
if (pipelines && pipelines.length) {
const lastCommitHash = rootGetters.lastCommit && rootGetters.lastCommit.id;
lastCommitPipeline = pipelines.find(pipeline => pipeline.commit.id === lastCommitHash);
}
commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, lastCommitPipeline);
}; };
export const receiveLatestPipelineSuccess = ({ commit }, pipeline) =>
commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, pipeline);
export const fetchLatestPipeline = ({ dispatch, rootState }, sha) => { export const fetchLatestPipeline = ({ dispatch, rootGetters }) => {
if (eTagPoll) return;
dispatch('requestLatestPipeline'); dispatch('requestLatestPipeline');
return Api.pipelines(rootState.currentProjectId, { sha, per_page: '1' }) eTagPoll = new Poll({
.then(({ data }) => { resource: service,
dispatch('receiveLatestPipelineSuccess', data.pop()); method: 'lastCommitPipelines',
}) data: { getters: rootGetters },
.catch(() => dispatch('receiveLatestPipelineError')); successCallback: ({ data }) => dispatch('receiveLatestPipelineSuccess', data),
errorCallback: () => dispatch('receiveLatestPipelineError'),
});
if (!Visibility.hidden()) {
eTagPoll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
eTagPoll.restart();
} else {
eTagPoll.stop();
}
});
}; };
export const requestJobs = ({ commit }) => commit(types.REQUEST_JOBS); export const requestJobs = ({ commit }, id) => commit(types.REQUEST_JOBS, id);
export const receiveJobsError = ({ commit }) => { export const receiveJobsError = ({ commit }, id) => {
flash(__('There was an error loading jobs')); flash(__('There was an error loading jobs'));
commit(types.RECEIVE_JOBS_ERROR); commit(types.RECEIVE_JOBS_ERROR, id);
}; };
export const receiveJobsSuccess = ({ commit }, data) => commit(types.RECEIVE_JOBS_SUCCESS, data); export const receiveJobsSuccess = ({ commit }, { id, data }) =>
commit(types.RECEIVE_JOBS_SUCCESS, { id, data });
export const fetchJobs = ({ dispatch, state, rootState }, page = '1') => { export const fetchJobs = ({ dispatch }, stage) => {
dispatch('requestJobs'); dispatch('requestJobs', stage.id);
Api.pipelineJobs(rootState.currentProjectId, state.latestPipeline.id, { axios
page, .get(stage.dropdownPath)
}) .then(({ data }) => dispatch('receiveJobsSuccess', { id: stage.id, data }))
.then(({ data, headers }) => { .catch(() => dispatch('receiveJobsError', stage.id));
const nextPage = headers && headers['x-next-page'];
dispatch('receiveJobsSuccess', data);
if (nextPage) {
dispatch('fetchJobs', nextPage);
}
})
.catch(() => dispatch('receiveJobsError'));
}; };
export const toggleStageCollapsed = ({ commit }, stageId) =>
commit(types.TOGGLE_STAGE_COLLAPSE, stageId);
export default () => {}; export default () => {};
// eslint-disable-next-line import/prefer-default-export
export const states = {
failed: 'failed',
};
import { states } from './constants';
export const hasLatestPipeline = state => !state.isLoadingPipeline && !!state.latestPipeline; export const hasLatestPipeline = state => !state.isLoadingPipeline && !!state.latestPipeline;
export const failedJobs = state => export const pipelineFailed = state =>
state.latestPipeline && state.latestPipeline.details.status.text === states.failed;
export const failedStages = state =>
state.stages.filter(stage => stage.status.text.toLowerCase() === states.failed).map(stage => ({
...stage,
jobs: stage.jobs.filter(job => job.status.text.toLowerCase() === states.failed),
}));
export const failedJobsCount = state =>
state.stages.reduce( state.stages.reduce(
(acc, stage) => acc.concat(stage.jobs.filter(job => job.status === 'failed')), (acc, stage) => acc + stage.jobs.filter(j => j.status.text === states.failed).length,
[], 0,
); );
export const jobsCount = state => state.stages.reduce((acc, stage) => acc + stage.jobs.length, 0);
export default () => {};
...@@ -5,3 +5,5 @@ export const RECEIVE_LASTEST_PIPELINE_SUCCESS = 'RECEIVE_LASTEST_PIPELINE_SUCCES ...@@ -5,3 +5,5 @@ export const RECEIVE_LASTEST_PIPELINE_SUCCESS = 'RECEIVE_LASTEST_PIPELINE_SUCCES
export const REQUEST_JOBS = 'REQUEST_JOBS'; export const REQUEST_JOBS = 'REQUEST_JOBS';
export const RECEIVE_JOBS_ERROR = 'RECEIVE_JOBS_ERROR'; export const RECEIVE_JOBS_ERROR = 'RECEIVE_JOBS_ERROR';
export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS'; export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS';
export const TOGGLE_STAGE_COLLAPSE = 'TOGGLE_STAGE_COLLAPSE';
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import * as types from './mutation_types'; import * as types from './mutation_types';
import { normalizeJob } from './utils';
export default { export default {
[types.REQUEST_LATEST_PIPELINE](state) { [types.REQUEST_LATEST_PIPELINE](state) {
...@@ -14,40 +15,52 @@ export default { ...@@ -14,40 +15,52 @@ export default {
if (pipeline) { if (pipeline) {
state.latestPipeline = { state.latestPipeline = {
id: pipeline.id, id: pipeline.id,
status: pipeline.status, path: pipeline.path,
commit: pipeline.commit,
details: {
status: pipeline.details.status,
},
yamlError: pipeline.yaml_errors,
}; };
state.stages = pipeline.details.stages.map((stage, i) => {
const foundStage = state.stages.find(s => s.id === i);
return {
id: i,
dropdownPath: stage.dropdown_path,
name: stage.name,
status: stage.status,
isCollapsed: foundStage ? foundStage.isCollapsed : false,
isLoading: foundStage ? foundStage.isLoading : false,
jobs: foundStage ? foundStage.jobs : [],
};
});
} else {
state.latestPipeline = false;
} }
}, },
[types.REQUEST_JOBS](state) { [types.REQUEST_JOBS](state, id) {
state.isLoadingJobs = true; state.stages = state.stages.map(stage => ({
...stage,
isLoading: stage.id === id ? true : stage.isLoading,
}));
}, },
[types.RECEIVE_JOBS_ERROR](state) { [types.RECEIVE_JOBS_ERROR](state, id) {
state.isLoadingJobs = false; state.stages = state.stages.map(stage => ({
...stage,
isLoading: stage.id === id ? false : stage.isLoading,
}));
}, },
[types.RECEIVE_JOBS_SUCCESS](state, jobs) { [types.RECEIVE_JOBS_SUCCESS](state, { id, data }) {
state.isLoadingJobs = false; state.stages = state.stages.map(stage => ({
...stage,
state.stages = jobs.reduce((acc, job) => { isLoading: stage.id === id ? false : stage.isLoading,
let stage = acc.find(s => s.title === job.stage); jobs: stage.id === id ? data.latest_statuses.map(normalizeJob) : stage.jobs,
}));
if (!stage) { },
stage = { [types.TOGGLE_STAGE_COLLAPSE](state, id) {
title: job.stage, state.stages = state.stages.map(stage => ({
jobs: [], ...stage,
}; isCollapsed: stage.id === id ? !stage.isCollapsed : stage.isCollapsed,
}));
acc.push(stage);
}
stage.jobs = stage.jobs.concat({
id: job.id,
name: job.name,
status: job.status,
stage: job.stage,
duration: job.duration,
});
return acc;
}, state.stages);
}, },
}; };
export default () => ({ export default () => ({
isLoadingPipeline: false, isLoadingPipeline: true,
isLoadingJobs: false, isLoadingJobs: false,
latestPipeline: null, latestPipeline: null,
stages: [], stages: [],
......
// eslint-disable-next-line import/prefer-default-export
export const normalizeJob = job => ({
id: job.id,
name: job.name,
status: job.status,
path: job.build_path,
});
...@@ -6,6 +6,7 @@ export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED'; ...@@ -6,6 +6,7 @@ export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED'; export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS'; export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS';
export const SET_EMPTY_STATE_SVGS = 'SET_EMPTY_STATE_SVGS'; export const SET_EMPTY_STATE_SVGS = 'SET_EMPTY_STATE_SVGS';
export const SET_LINKS = 'SET_LINKS';
// Project Mutation Types // Project Mutation Types
export const SET_PROJECT = 'SET_PROJECT'; export const SET_PROJECT = 'SET_PROJECT';
...@@ -23,7 +24,6 @@ export const SET_BRANCH = 'SET_BRANCH'; ...@@ -23,7 +24,6 @@ export const SET_BRANCH = 'SET_BRANCH';
export const SET_BRANCH_COMMIT = 'SET_BRANCH_COMMIT'; export const SET_BRANCH_COMMIT = 'SET_BRANCH_COMMIT';
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE'; export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN'; export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
export const SET_LAST_COMMIT_PIPELINE = 'SET_LAST_COMMIT_PIPELINE';
// Tree mutation types // Tree mutation types
export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA'; export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
...@@ -66,3 +66,5 @@ export const UPDATE_ACTIVITY_BAR_VIEW = 'UPDATE_ACTIVITY_BAR_VIEW'; ...@@ -66,3 +66,5 @@ export const UPDATE_ACTIVITY_BAR_VIEW = 'UPDATE_ACTIVITY_BAR_VIEW';
export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG'; export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER'; export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL'; export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL';
export const SET_RIGHT_PANE = 'SET_RIGHT_PANE';
...@@ -114,12 +114,13 @@ export default { ...@@ -114,12 +114,13 @@ export default {
}, },
[types.SET_EMPTY_STATE_SVGS]( [types.SET_EMPTY_STATE_SVGS](
state, state,
{ emptyStateSvgPath, noChangesStateSvgPath, committedStateSvgPath }, { emptyStateSvgPath, noChangesStateSvgPath, committedStateSvgPath, pipelinesEmptyStateSvgPath },
) { ) {
Object.assign(state, { Object.assign(state, {
emptyStateSvgPath, emptyStateSvgPath,
noChangesStateSvgPath, noChangesStateSvgPath,
committedStateSvgPath, committedStateSvgPath,
pipelinesEmptyStateSvgPath,
}); });
}, },
[types.TOGGLE_FILE_FINDER](state, fileFindVisible) { [types.TOGGLE_FILE_FINDER](state, fileFindVisible) {
...@@ -148,6 +149,14 @@ export default { ...@@ -148,6 +149,14 @@ export default {
unusedSeal: false, unusedSeal: false,
}); });
}, },
[types.SET_RIGHT_PANE](state, view) {
Object.assign(state, {
rightPane: state.rightPane === view ? null : view,
});
},
[types.SET_LINKS](state, links) {
Object.assign(state, { links });
},
...projectMutations, ...projectMutations,
...mergeRequestMutation, ...mergeRequestMutation,
...fileMutations, ...fileMutations,
......
...@@ -14,10 +14,6 @@ export default { ...@@ -14,10 +14,6 @@ export default {
treeId: `${projectPath}/${branchName}`, treeId: `${projectPath}/${branchName}`,
active: true, active: true,
workingReference: '', workingReference: '',
commit: {
...branch.commit,
pipeline: {},
},
}, },
}, },
}); });
...@@ -32,9 +28,4 @@ export default { ...@@ -32,9 +28,4 @@ export default {
commit, commit,
}); });
}, },
[types.SET_LAST_COMMIT_PIPELINE](state, { projectId, branchId, pipeline }) {
Object.assign(state.projects[projectId].branches[branchId].commit, {
pipeline,
});
},
}; };
...@@ -23,4 +23,6 @@ export default () => ({ ...@@ -23,4 +23,6 @@ export default () => ({
currentActivityView: activityBarViews.edit, currentActivityView: activityBarViews.edit,
unusedSeal: true, unusedSeal: true,
fileFindVisible: false, fileFindVisible: false,
rightPane: null,
links: {},
}); });
...@@ -30,7 +30,7 @@ export default class IssuableForm { ...@@ -30,7 +30,7 @@ export default class IssuableForm {
} }
this.initAutosave(); this.initAutosave();
this.form.on('submit:success', this.handleSubmit); this.form.on('submit', this.handleSubmit);
this.form.on('click', '.btn-cancel', this.resetAutosave); this.form.on('click', '.btn-cancel', this.resetAutosave);
this.initWip(); this.initWip();
......
...@@ -84,7 +84,7 @@ export default class Job { ...@@ -84,7 +84,7 @@ export default class Job {
If the browser does not support position sticky, it returns the position as static. If the browser does not support position sticky, it returns the position as static.
If the browser does support sticky, then we allow the browser to handle it, if not If the browser does support sticky, then we allow the browser to handle it, if not
then we use a polyfill then we use a polyfill
**/ */
if (this.$topBar.css('position') !== 'static') return; if (this.$topBar.css('position') !== 'static') return;
StickyFill.add(this.$topBar); StickyFill.add(this.$topBar);
......
/* global Build */
import Visibility from 'visibilityjs'; import Visibility from 'visibilityjs';
import Flash from '../flash'; import Flash from '../flash';
import Poll from '../lib/utils/poll'; import Poll from '../lib/utils/poll';
...@@ -50,7 +48,8 @@ export default class JobMediator { ...@@ -50,7 +48,8 @@ export default class JobMediator {
} }
getJob() { getJob() {
return this.service.getJob() return this.service
.getJob()
.then(response => this.successCallback(response)) .then(response => this.successCallback(response))
.catch(() => this.errorCallback()); .catch(() => this.errorCallback());
} }
......
/* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */ /* eslint-disable comma-dangle, class-methods-use-this, no-underscore-dangle, no-param-reassign, no-unused-vars, consistent-return, func-names, space-before-function-paren, max-len */
import $ from 'jquery'; import $ from 'jquery';
import Sortable from 'vendor/Sortable'; import Sortable from 'sortablejs';
import flash from './flash'; import flash from './flash';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
......
...@@ -9,7 +9,7 @@ delete window.translations; ...@@ -9,7 +9,7 @@ delete window.translations;
Translates `text` Translates `text`
@param text The text to be translated @param text The text to be translated
@returns {String} The translated text @returns {String} The translated text
**/ */
const gettext = locale.gettext.bind(locale); const gettext = locale.gettext.bind(locale);
/** /**
...@@ -21,7 +21,7 @@ const gettext = locale.gettext.bind(locale); ...@@ -21,7 +21,7 @@ const gettext = locale.gettext.bind(locale);
@param pluralText Plural text to translate (eg. '%d days') @param pluralText Plural text to translate (eg. '%d days')
@param count Number to decide which translation to use (eg. 2) @param count Number to decide which translation to use (eg. 2)
@returns {String} Translated text with the number replaced (eg. '2 days') @returns {String} Translated text with the number replaced (eg. '2 days')
**/ */
const ngettext = (text, pluralText, count) => { const ngettext = (text, pluralText, count) => {
const translated = locale.ngettext(text, pluralText, count).replace(/%d/g, count).split('|'); const translated = locale.ngettext(text, pluralText, count).replace(/%d/g, count).split('|');
...@@ -38,7 +38,7 @@ const ngettext = (text, pluralText, count) => { ...@@ -38,7 +38,7 @@ const ngettext = (text, pluralText, count) => {
(eg. 'Context') (eg. 'Context')
@param key Is the dynamic variable you want to be translated @param key Is the dynamic variable you want to be translated
@returns {String} Translated context based text @returns {String} Translated context based text
**/ */
const pgettext = (keyOrContext, key) => { const pgettext = (keyOrContext, key) => {
const normalizedKey = key ? `${keyOrContext}|${key}` : keyOrContext; const normalizedKey = key ? `${keyOrContext}|${key}` : keyOrContext;
const translated = gettext(normalizedKey).split('|'); const translated = gettext(normalizedKey).split('|');
......
...@@ -10,7 +10,7 @@ import _ from 'underscore'; ...@@ -10,7 +10,7 @@ import _ from 'underscore';
@see https://ruby-doc.org/core-2.3.3/Kernel.html#method-i-sprintf @see https://ruby-doc.org/core-2.3.3/Kernel.html#method-i-sprintf
@see https://gitlab.com/gitlab-org/gitlab-ce/issues/37992 @see https://gitlab.com/gitlab-org/gitlab-ce/issues/37992
**/ */
export default (input, parameters, escapeParameters = true) => { export default (input, parameters, escapeParameters = true) => {
let output = input; let output = input;
......
...@@ -427,7 +427,7 @@ export default class MergeRequestTabs { ...@@ -427,7 +427,7 @@ export default class MergeRequestTabs {
If the browser does not support position sticky, it returns the position as static. If the browser does not support position sticky, it returns the position as static.
If the browser does support sticky, then we allow the browser to handle it, if not If the browser does support sticky, then we allow the browser to handle it, if not
then we default back to Bootstraps affix then we default back to Bootstraps affix
**/ */
if ($tabs.css('position') !== 'static') return; if ($tabs.css('position') !== 'static') return;
const $diffTabs = $('#diff-notes-app'); const $diffTabs = $('#diff-notes-app');
......
...@@ -12,20 +12,13 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils'; ...@@ -12,20 +12,13 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
let eTagPoll; let eTagPoll;
export const setNotesData = ({ commit }, data) => export const setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data);
commit(types.SET_NOTES_DATA, data); export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data);
export const setNoteableData = ({ commit }, data) => export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data);
commit(types.SET_NOTEABLE_DATA, data); export const setLastFetchedAt = ({ commit }, data) => commit(types.SET_LAST_FETCHED_AT, data);
export const setUserData = ({ commit }, data) => export const setInitialNotes = ({ commit }, data) => commit(types.SET_INITIAL_NOTES, data);
commit(types.SET_USER_DATA, data); export const setTargetNoteHash = ({ commit }, data) => commit(types.SET_TARGET_NOTE_HASH, data);
export const setLastFetchedAt = ({ commit }, data) => export const toggleDiscussion = ({ commit }, data) => commit(types.TOGGLE_DISCUSSION, data);
commit(types.SET_LAST_FETCHED_AT, data);
export const setInitialNotes = ({ commit }, data) =>
commit(types.SET_INITIAL_NOTES, data);
export const setTargetNoteHash = ({ commit }, data) =>
commit(types.SET_TARGET_NOTE_HASH, data);
export const toggleDiscussion = ({ commit }, data) =>
commit(types.TOGGLE_DISCUSSION, data);
export const fetchNotes = ({ commit }, path) => export const fetchNotes = ({ commit }, path) =>
service service
...@@ -69,20 +62,14 @@ export const createNewNote = ({ commit }, { endpoint, data }) => ...@@ -69,20 +62,14 @@ export const createNewNote = ({ commit }, { endpoint, data }) =>
return res; return res;
}); });
export const removePlaceholderNotes = ({ commit }) => export const removePlaceholderNotes = ({ commit }) => commit(types.REMOVE_PLACEHOLDER_NOTES);
commit(types.REMOVE_PLACEHOLDER_NOTES);
export const toggleResolveNote = ( export const toggleResolveNote = ({ commit }, { endpoint, isResolved, discussion }) =>
{ commit },
{ endpoint, isResolved, discussion },
) =>
service service
.toggleResolveNote(endpoint, isResolved) .toggleResolveNote(endpoint, isResolved)
.then(res => res.json()) .then(res => res.json())
.then(res => { .then(res => {
const mutationType = discussion const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE;
? types.UPDATE_DISCUSSION
: types.UPDATE_NOTE;
commit(mutationType, res); commit(mutationType, res);
}); });
...@@ -114,7 +101,7 @@ export const reopenIssue = ({ commit, dispatch, state }) => { ...@@ -114,7 +101,7 @@ export const reopenIssue = ({ commit, dispatch, state }) => {
export const toggleStateButtonLoading = ({ commit }, value) => export const toggleStateButtonLoading = ({ commit }, value) =>
commit(types.TOGGLE_STATE_BUTTON_LOADING, value); commit(types.TOGGLE_STATE_BUTTON_LOADING, value);
export const emitStateChangedEvent = ({ commit, getters }, data) => { export const emitStateChangedEvent = ({ getters }, data) => {
const event = new CustomEvent('issuable_vue_app:change', { const event = new CustomEvent('issuable_vue_app:change', {
detail: { detail: {
data, data,
...@@ -179,10 +166,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { ...@@ -179,10 +166,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
loadAwardsHandler() loadAwardsHandler()
.then(awardsHandler => { .then(awardsHandler => {
awardsHandler.addAwardToEmojiBar( awardsHandler.addAwardToEmojiBar(votesBlock, commandsChanges.emoji_award);
votesBlock,
commandsChanges.emoji_award,
);
awardsHandler.scrollToAwards(); awardsHandler.scrollToAwards();
}) })
.catch(() => { .catch(() => {
...@@ -194,10 +178,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => { ...@@ -194,10 +178,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
}); });
} }
if ( if (commandsChanges.spend_time != null || commandsChanges.time_estimate != null) {
commandsChanges.spend_time != null ||
commandsChanges.time_estimate != null
) {
sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res); sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res);
} }
} }
...@@ -218,14 +199,8 @@ const pollSuccessCallBack = (resp, commit, state, getters) => { ...@@ -218,14 +199,8 @@ const pollSuccessCallBack = (resp, commit, state, getters) => {
resp.notes.forEach(note => { resp.notes.forEach(note => {
if (notesById[note.id]) { if (notesById[note.id]) {
commit(types.UPDATE_NOTE, note); commit(types.UPDATE_NOTE, note);
} else if ( } else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) {
note.type === constants.DISCUSSION_NOTE || const discussion = utils.findNoteObjectById(state.notes, note.discussion_id);
note.type === constants.DIFF_NOTE
) {
const discussion = utils.findNoteObjectById(
state.notes,
note.discussion_id,
);
if (discussion) { if (discussion) {
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note); commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note);
...@@ -249,11 +224,8 @@ export const poll = ({ commit, state, getters }) => { ...@@ -249,11 +224,8 @@ export const poll = ({ commit, state, getters }) => {
method: 'poll', method: 'poll',
data: state, data: state,
successCallback: resp => successCallback: resp =>
resp resp.json().then(data => pollSuccessCallBack(data, commit, state, getters)),
.json() errorCallback: () => Flash('Something went wrong while fetching latest comments.'),
.then(data => pollSuccessCallBack(data, commit, state, getters)),
errorCallback: () =>
Flash('Something went wrong while fetching latest comments.'),
}); });
if (!Visibility.hidden()) { if (!Visibility.hidden()) {
...@@ -292,14 +264,11 @@ export const fetchData = ({ commit, state, getters }) => { ...@@ -292,14 +264,11 @@ export const fetchData = ({ commit, state, getters }) => {
.catch(() => Flash('Something went wrong while fetching latest comments.')); .catch(() => Flash('Something went wrong while fetching latest comments.'));
}; };
export const toggleAward = ( export const toggleAward = ({ commit, getters }, { awardName, noteId }) => {
{ commit, state, getters, dispatch },
{ awardName, noteId },
) => {
commit(types.TOGGLE_AWARD, { awardName, note: getters.notesById[noteId] }); commit(types.TOGGLE_AWARD, { awardName, note: getters.notesById[noteId] });
}; };
export const toggleAwardRequest = ({ commit, getters, dispatch }, data) => { export const toggleAwardRequest = ({ dispatch }, data) => {
const { endpoint, awardName } = data; const { endpoint, awardName } = data;
return service return service
......
...@@ -59,7 +59,7 @@ export default { ...@@ -59,7 +59,7 @@ export default {
ref="form" ref="form"
:action="deleteWikiUrl" :action="deleteWikiUrl"
method="post" method="post"
class="form-horizontal js-requires-input" class="js-requires-input"
> >
<input <input
ref="method" ref="method"
......
...@@ -5,7 +5,7 @@ import $ from 'jquery'; ...@@ -5,7 +5,7 @@ import $ from 'jquery';
* *
* Toggling this checkbox adds/removes a `remember_me` parameter to the * Toggling this checkbox adds/removes a `remember_me` parameter to the
* login buttons' href, which is passed on to the omniauth callback. * login buttons' href, which is passed on to the omniauth callback.
**/ */
export default class OAuthRememberMe { export default class OAuthRememberMe {
constructor(opts = {}) { constructor(opts = {}) {
......
...@@ -79,12 +79,13 @@ export default { ...@@ -79,12 +79,13 @@ export default {
}; };
</script> </script>
<template> <template>
<div class="ci-job-dropdown-container dropdown"> <div class="ci-job-dropdown-container dropdown dropright">
<button <button
v-tooltip v-tooltip
type="button" type="button"
data-toggle="dropdown" data-toggle="dropdown"
data-container="body" data-container="body"
data-boundary="viewport"
class="dropdown-menu-toggle build-content" class="dropdown-menu-toggle build-content"
:title="tooltipText" :title="tooltipText"
> >
......
...@@ -37,7 +37,7 @@ const IGNORE_URLS = [ ...@@ -37,7 +37,7 @@ const IGNORE_URLS = [
/extensions\//i, /extensions\//i,
/^chrome:\/\//i, /^chrome:\/\//i,
// Other plugins // Other plugins
/127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb /127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb
/webappstoolbarba\.texthelp\.com\//i, /webappstoolbarba\.texthelp\.com\//i,
/metrics\.itunes\.apple\.com\.edgesuite\.net\//i, /metrics\.itunes\.apple\.com\.edgesuite\.net\//i,
]; ];
......
...@@ -7,9 +7,10 @@ Vue.use(VueResource); ...@@ -7,9 +7,10 @@ Vue.use(VueResource);
export const fetchRepos = ({ commit, state }) => { export const fetchRepos = ({ commit, state }) => {
commit(types.TOGGLE_MAIN_LOADING); commit(types.TOGGLE_MAIN_LOADING);
return Vue.http.get(state.endpoint) return Vue.http
.get(state.endpoint)
.then(res => res.json()) .then(res => res.json())
.then((response) => { .then(response => {
commit(types.TOGGLE_MAIN_LOADING); commit(types.TOGGLE_MAIN_LOADING);
commit(types.SET_REPOS_LIST, response); commit(types.SET_REPOS_LIST, response);
}); });
...@@ -18,19 +19,20 @@ export const fetchRepos = ({ commit, state }) => { ...@@ -18,19 +19,20 @@ export const fetchRepos = ({ commit, state }) => {
export const fetchList = ({ commit }, { repo, page }) => { export const fetchList = ({ commit }, { repo, page }) => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo); commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
return Vue.http.get(repo.tagsPath, { params: { page } }) return Vue.http.get(repo.tagsPath, { params: { page } }).then(response => {
.then((response) => { const headers = response.headers;
const headers = response.headers;
return response.json().then((resp) => { return response.json().then(resp => {
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo); commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
commit(types.SET_REGISTRY_LIST, { repo, resp, headers }); commit(types.SET_REGISTRY_LIST, { repo, resp, headers });
});
}); });
});
}; };
// eslint-disable-next-line no-unused-vars
export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath); export const deleteRepo = ({ commit }, repo) => Vue.http.delete(repo.destroyPath);
// eslint-disable-next-line no-unused-vars
export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath); export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath);
export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data); export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
......
...@@ -168,8 +168,8 @@ ...@@ -168,8 +168,8 @@
<a <a
:href="mr.mergeCommitPath" :href="mr.mergeCommitPath"
class="commit-sha js-mr-merged-commit-sha" class="commit-sha js-mr-merged-commit-sha"
v-text="mr.shortMergeCommitSha"
> >
{{ mr.shortMergeCommitSha }}
</a> </a>
<clipboard-button <clipboard-button
:title="__('Copy commit SHA to clipboard')" :title="__('Copy commit SHA to clipboard')"
......
...@@ -22,6 +22,8 @@ import Icon from '../../vue_shared/components/icon.vue'; ...@@ -22,6 +22,8 @@ import Icon from '../../vue_shared/components/icon.vue';
* - Jobs show view header * - Jobs show view header
* - Jobs show view sidebar * - Jobs show view sidebar
*/ */
const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
export default { export default {
components: { components: {
Icon, Icon,
...@@ -31,17 +33,36 @@ export default { ...@@ -31,17 +33,36 @@ export default {
type: Object, type: Object,
required: true, required: true,
}, },
size: {
type: Number,
required: false,
default: 16,
validator(value) {
return validSizes.includes(value);
},
},
borderless: {
type: Boolean,
required: false,
default: false,
},
}, },
computed: { computed: {
cssClass() { cssClass() {
const status = this.status.group; const status = this.status.group;
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`; return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
}, },
icon() {
return this.borderless ? `${this.status.icon}_borderless` : this.status.icon;
},
}, },
}; };
</script> </script>
<template> <template>
<span :class="cssClass"> <span :class="cssClass">
<icon :name="status.icon" /> <icon
:name="icon"
:size="size"
/>
</span> </span>
</template> </template>
<script>
export default {
props: {
title: {
type: String,
required: false,
default: '',
},
active: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
// props can't be updated, so we map it to data where we can
localActive: this.active,
};
},
watch: {
active() {
this.localActive = this.active;
},
},
created() {
this.isTab = true;
},
};
</script>
<template>
<div
class="tab-pane"
:class="{
active: localActive
}"
role="tabpanel"
>
<slot></slot>
</div>
</template>
export default {
data() {
return {
currentIndex: 0,
tabs: [],
};
},
mounted() {
this.updateTabs();
},
methods: {
updateTabs() {
this.tabs = this.$children.filter(child => child.isTab);
this.currentIndex = this.tabs.findIndex(tab => tab.localActive);
},
setTab(index) {
this.tabs[this.currentIndex].localActive = false;
this.tabs[index].localActive = true;
this.currentIndex = index;
},
},
render(h) {
const navItems = this.tabs.map((tab, i) =>
h(
'li',
{
key: i,
},
[
h(
'a',
{
class: tab.localActive ? 'active' : null,
attrs: {
href: '#',
},
on: {
click: () => this.setTab(i),
},
},
tab.$slots.title || tab.title,
),
],
),
);
const nav = h(
'ul',
{
class: 'nav-links tab-links',
},
[navItems],
);
const content = h(
'div',
{
class: ['tab-content'],
},
[this.$slots.default],
);
return h('div', {}, [[nav], content]);
},
};
...@@ -2,7 +2,9 @@ import $ from 'jquery'; ...@@ -2,7 +2,9 @@ import $ from 'jquery';
export default { export default {
bind(el) { bind(el) {
$(el).tooltip(); $(el).tooltip({
trigger: 'hover',
});
}, },
componentUpdated(el) { componentUpdated(el) {
......
...@@ -13,7 +13,7 @@ export default (Vue) => { ...@@ -13,7 +13,7 @@ export default (Vue) => {
@param text The text to be translated @param text The text to be translated
@returns {String} The translated text @returns {String} The translated text
**/ */
__, __,
/** /**
Translate the text with a number Translate the text with a number
...@@ -24,7 +24,7 @@ export default (Vue) => { ...@@ -24,7 +24,7 @@ export default (Vue) => {
@param pluralText Plural text to translate (eg. '%d days') @param pluralText Plural text to translate (eg. '%d days')
@param count Number to decide which translation to use (eg. 2) @param count Number to decide which translation to use (eg. 2)
@returns {String} Translated text with the number replaced (eg. '2 days') @returns {String} Translated text with the number replaced (eg. '2 days')
**/ */
n__, n__,
/** /**
Translate context based text Translate context based text
...@@ -36,7 +36,7 @@ export default (Vue) => { ...@@ -36,7 +36,7 @@ export default (Vue) => {
(eg. 'Context') (eg. 'Context')
@param key Is the dynamic variable you want to be translated @param key Is the dynamic variable you want to be translated
@returns {String} Translated context based text @returns {String} Translated context based text
**/ */
s__, s__,
sprintf, sprintf,
}, },
......
...@@ -87,7 +87,8 @@ table { ...@@ -87,7 +87,8 @@ table {
display: none; display: none;
} }
.dropdown-toggle::after { .dropdown-toggle::after,
.dropright .dropdown-menu-toggle::after {
// Remove bootstrap's dropdown caret // Remove bootstrap's dropdown caret
display: none; display: none;
} }
...@@ -148,8 +149,14 @@ table { ...@@ -148,8 +149,14 @@ table {
} }
} }
.nav-tabs .nav-link { .nav-tabs {
border: 0; .nav-link {
border: 0;
}
.nav-item {
margin-bottom: 0;
}
} }
pre code { pre code {
......
/*
* This is a minimal stylesheet, meant to be used for error pages.
*/
@import 'framework/variables';
@import '../../../node_modules/bootstrap/scss/functions';
@import '../../../node_modules/bootstrap/scss/variables';
@import '../../../node_modules/bootstrap/scss/mixins';
@import '../../../node_modules/bootstrap/scss/reboot';
@import '../../../node_modules/bootstrap/scss/buttons';
@import '../../../node_modules/bootstrap/scss/forms';
$body-color: #666;
$header-color: #456;
body {
color: $body-color;
text-align: center;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
margin: auto;
font-size: 14px;
}
h1 {
font-size: 56px;
line-height: 100px;
font-weight: 400;
color: $header-color;
}
h2 {
font-size: 24px;
color: $body-color;
line-height: 1.5em;
}
h3 {
color: $header-color;
font-size: 20px;
font-weight: 400;
line-height: 28px;
}
img {
max-width: 80vw;
display: block;
margin: 40px auto;
}
a {
text-decoration: none;
color: $blue-600;
}
.page-container {
margin: auto 20px;
}
.container {
margin: auto;
max-width: 600px;
border-bottom: 1px solid $border-color;
padding-bottom: 1em;
}
.action-container {
padding: 0.5em 0;
}
.form-inline-flex {
display: flex;
flex-wrap: wrap;
button {
display: block;
width: 100%;
}
.field {
display: block;
width: 100%;
margin-bottom: 1em;
}
@include media-breakpoint-up(sm) {
flex-wrap: nowrap;
button {
width: auto;
}
.field {
margin-bottom: 0;
margin-right: 0.5em;
}
}
}
.error-nav {
padding: 0;
text-align: center;
li {
display: block;
padding-bottom: 1em;
}
@include media-breakpoint-up(sm) {
li {
display: inline-block;
padding-bottom: 0;
&:not(:first-child)::before {
content: '\00B7';
display: inline-block;
padding: 0 1em;
}
}
}
}
...@@ -169,11 +169,14 @@ ...@@ -169,11 +169,14 @@
color: $color-800; color: $color-800;
} }
.nav-links li a.active { .nav-links li {
border-bottom: 2px solid $color-500; &.active a,
a.active {
border-bottom: 2px solid $color-500;
.badge.badge-pill { .badge.badge-pill {
font-weight: $gl-font-weight-bold; font-weight: $gl-font-weight-bold;
}
} }
} }
...@@ -189,6 +192,10 @@ ...@@ -189,6 +192,10 @@
&.active { &.active {
color: $color-700; color: $color-700;
box-shadow: inset 3px 0 $color-700; box-shadow: inset 3px 0 $color-700;
&.is-right {
box-shadow: inset -3px 0 $color-700;
}
} }
} }
} }
......
...@@ -19,14 +19,23 @@ ...@@ -19,14 +19,23 @@
width: auto; width: auto;
display: inline-block; display: inline-block;
overflow-x: auto; overflow-x: auto;
border-left: 0; border: 0;
border-right: 0; border-color: $md-area-border;
border-bottom: 0;
@supports(width: fit-content) { @supports(width: fit-content) {
display: block; display: block;
width: fit-content; width: fit-content;
} }
tr {
th {
border-bottom: solid 2px $md-area-border;
}
td {
border-color: $md-area-border;
}
}
} }
/* /*
......
...@@ -74,12 +74,6 @@ body.modal-open { ...@@ -74,12 +74,6 @@ body.modal-open {
} }
} }
@include media-breakpoint-up(lg) {
.modal-full {
width: 98%;
}
}
.modal { .modal {
background-color: $black-transparent; background-color: $black-transparent;
z-index: 2100; z-index: 2100;
......
...@@ -31,14 +31,15 @@ ...@@ -31,14 +31,15 @@
color: $black; color: $black;
} }
} }
}
&.active { &.active a,
color: $black; a.active {
font-weight: $gl-font-weight-bold; color: $black;
font-weight: $gl-font-weight-bold;
.badge.badge-pill { .badge.badge-pill {
color: $black; color: $black;
}
} }
} }
} }
......
...@@ -49,26 +49,11 @@ ...@@ -49,26 +49,11 @@
margin-top: 15px; margin-top: 15px;
} }
.snippet-embed-input {
height: 35px;
}
.embed-snippet { .embed-snippet {
padding-right: 0; padding-right: 0;
padding-top: $gl-padding; padding-top: $gl-padding;
.form-control {
cursor: auto;
width: 101%;
margin-left: -1px;
}
.embed-toggle-list li button { .embed-toggle-list li button {
padding: 8px 40px; padding: 8px 40px;
} }
.embed-toggle,
.snippet-clipboard-btn {
height: 35px;
}
} }
...@@ -373,6 +373,7 @@ $dropdown-chevron-size: 10px; ...@@ -373,6 +373,7 @@ $dropdown-chevron-size: 10px;
$dropdown-toggle-active-border-color: darken($border-color, 14%); $dropdown-toggle-active-border-color: darken($border-color, 14%);
$dropdown-item-hover-bg: $gray-darker; $dropdown-item-hover-bg: $gray-darker;
$dropdown-fade-mask-height: 32px; $dropdown-fade-mask-height: 32px;
$dropdown-member-form-control-width: 163px;
/* /*
* Filtered Search * Filtered Search
......
...@@ -6,7 +6,7 @@ ...@@ -6,7 +6,7 @@
.cluster-applications-table { .cluster-applications-table {
// Wait for the Vue to kick-in and render the applications block // Wait for the Vue to kick-in and render the applications block
min-height: 400px; min-height: 628px;
} }
.clusters-dropdown-menu { .clusters-dropdown-menu {
......
...@@ -36,13 +36,12 @@ ...@@ -36,13 +36,12 @@
} }
} }
.form-horizontal { .form-group {
margin-top: 20px; margin-bottom: 0;
@include media-breakpoint-up(sm) { @include media-breakpoint-down(sm) {
display: -webkit-flex; display: block;
display: flex; margin-left: 5px;
margin-top: 3px;
} }
} }
...@@ -62,10 +61,15 @@ ...@@ -62,10 +61,15 @@
} }
.member-form-control { .member-form-control {
@include media-breakpoint-down(xs) { @include media-breakpoint-down(sm) {
padding-bottom: 5px; width: $dropdown-member-form-control-width;
margin-left: 0; margin-left: 0;
padding-bottom: 5px;
}
@include media-breakpoint-down(xs) {
margin-right: 0; margin-right: 0;
width: auto;
} }
} }
...@@ -207,10 +211,6 @@ ...@@ -207,10 +211,6 @@
align-self: flex-start; align-self: flex-start;
} }
.form-horizontal ~ .btn {
margin-right: 0;
}
@include media-breakpoint-down(xs) { @include media-breakpoint-down(xs) {
display: block; display: block;
...@@ -220,6 +220,12 @@ ...@@ -220,6 +220,12 @@
display: block; display: block;
} }
.controls > .btn:last-child {
margin-left: 5px;
margin-right: 5px;
width: auto;
}
.form-control { .form-control {
width: 100%; width: 100%;
} }
...@@ -232,10 +238,6 @@ ...@@ -232,10 +238,6 @@
.member-controls { .member-controls {
margin-top: 5px; margin-top: 5px;
} }
.form-horizontal {
margin-top: 10px;
}
} }
} }
...@@ -259,10 +261,6 @@ ...@@ -259,10 +261,6 @@
margin-top: 0; margin-top: 0;
} }
.form-horizontal {
display: block;
}
.member-form-control { .member-form-control {
margin: 5px 0; margin: 5px 0;
} }
......
...@@ -909,6 +909,16 @@ ...@@ -909,6 +909,16 @@
width: 1px; width: 1px;
background: $white-light; background: $white-light;
} }
&.is-right {
padding-right: $gl-padding;
padding-left: $gl-padding + 1px;
&::after {
right: auto;
left: -1px;
}
}
} }
} }
...@@ -1121,3 +1131,112 @@ ...@@ -1121,3 +1131,112 @@
white-space: nowrap; white-space: nowrap;
} }
} }
.ide-external-link {
svg {
display: none;
}
&:hover,
&:focus {
svg {
display: inline-block;
}
}
}
.ide-right-sidebar {
width: auto;
min-width: 60px;
.ide-activity-bar {
border-left: 1px solid $white-dark;
}
.multi-file-commit-panel-inner {
width: 350px;
padding: $grid-size $gl-padding;
background-color: $white-light;
border-left: 1px solid $white-dark;
}
}
.ide-pipeline {
display: flex;
flex-direction: column;
height: 100%;
.empty-state {
margin-top: auto;
margin-bottom: auto;
p {
margin: $grid-size 0;
text-align: center;
line-height: 24px;
}
.btn,
h4 {
margin: 0;
}
}
}
.ide-pipeline-list {
flex: 1;
overflow: auto;
}
.ide-pipeline-header {
min-height: 50px;
padding-left: $gl-padding;
padding-right: $gl-padding;
.ci-status-icon {
display: flex;
}
}
.ide-job-item {
display: flex;
padding: 16px;
&:not(:last-child) {
border-bottom: 1px solid $border-color;
}
.ci-status-icon {
display: flex;
justify-content: center;
height: 20px;
margin-top: -2px;
overflow: hidden;
}
}
.ide-stage {
.card-header {
display: flex;
cursor: pointer;
.ci-status-icon {
display: flex;
align-items: center;
}
}
.card-body {
padding: 0;
}
}
.ide-stage-collapse-icon {
margin: auto 0 auto auto;
}
.ide-stage-title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
...@@ -146,14 +146,15 @@ class ApplicationController < ActionController::Base ...@@ -146,14 +146,15 @@ class ApplicationController < ActionController::Base
end end
def render_403 def render_403
head :forbidden respond_to do |format|
format.any { head :forbidden }
format.html { render "errors/access_denied", layout: "errors", status: 403 }
end
end end
def render_404 def render_404
respond_to do |format| respond_to do |format|
format.html do format.html { render "errors/not_found", layout: "errors", status: 404 }
render file: Rails.root.join("public", "404"), layout: false, status: "404"
end
# Prevent the Rails CSRF protector from thinking a missing .js file is a JavaScript file # Prevent the Rails CSRF protector from thinking a missing .js file is a JavaScript file
format.js { render json: '', status: :not_found, content_type: 'application/json' } format.js { render json: '', status: :not_found, content_type: 'application/json' }
format.any { head :not_found } format.any { head :not_found }
......
...@@ -17,10 +17,23 @@ module IssuesAction ...@@ -17,10 +17,23 @@ module IssuesAction
end end
# rubocop:enable Gitlab/ModuleWithInstanceVariables # rubocop:enable Gitlab/ModuleWithInstanceVariables
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def issues_calendar
@issues = issuables_collection
.non_archived
.with_due_date
.limit(100)
respond_to do |format|
format.ics { response.headers['Content-Disposition'] = 'inline' }
end
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
private private
def finder_type def finder_type
(super if defined?(super)) || (super if defined?(super)) ||
(IssuesFinder if action_name == 'issues') (IssuesFinder if %w(issues issues_calendar).include?(action_name))
end end
end end
module Groups
class SharedProjectsController < Groups::ApplicationController
respond_to :json
before_action :group
skip_cross_project_access_check :index
def index
shared_projects = GroupProjectsFinder.new(
group: group,
current_user: current_user,
params: finder_params,
options: { only_shared: true }
).execute
serializer = GroupChildSerializer.new(current_user: current_user)
.with_pagination(request, response)
render json: serializer.represent(shared_projects)
end
private
def finder_params
@finder_params ||= begin
# Make the `search` param consistent for the frontend,
# which will be using `filter`.
params[:search] ||= params[:filter] if params[:filter]
params.permit(:sort, :search)
end
end
end
end
...@@ -34,12 +34,12 @@ class ProfilesController < Profiles::ApplicationController ...@@ -34,12 +34,12 @@ class ProfilesController < Profiles::ApplicationController
redirect_to profile_personal_access_tokens_path redirect_to profile_personal_access_tokens_path
end end
def reset_rss_token def reset_feed_token
Users::UpdateService.new(current_user, user: @user).execute! do |user| Users::UpdateService.new(current_user, user: @user).execute! do |user|
user.reset_rss_token! user.reset_feed_token!
end end
flash[:notice] = "RSS token was successfully reset" flash[:notice] = 'Feed token was successfully reset'
redirect_to profile_personal_access_tokens_path redirect_to profile_personal_access_tokens_path
end end
......
...@@ -5,7 +5,17 @@ class Projects::Clusters::ApplicationsController < Projects::ApplicationControll ...@@ -5,7 +5,17 @@ class Projects::Clusters::ApplicationsController < Projects::ApplicationControll
before_action :authorize_create_cluster!, only: [:create] before_action :authorize_create_cluster!, only: [:create]
def create def create
application = @application_class.find_or_create_by!(cluster: @cluster) application = @application_class.find_or_initialize_by(cluster: @cluster)
if application.has_attribute?(:hostname)
application.hostname = params[:hostname]
end
if application.respond_to?(:oauth_application)
application.oauth_application = create_oauth_application(application)
end
application.save!
Clusters::Applications::ScheduleInstallationService.new(project, current_user).execute(application) Clusters::Applications::ScheduleInstallationService.new(project, current_user).execute(application)
...@@ -23,4 +33,15 @@ class Projects::Clusters::ApplicationsController < Projects::ApplicationControll ...@@ -23,4 +33,15 @@ class Projects::Clusters::ApplicationsController < Projects::ApplicationControll
def application_class def application_class
@application_class ||= Clusters::Cluster::APPLICATIONS[params[:application]] || render_404 @application_class ||= Clusters::Cluster::APPLICATIONS[params[:application]] || render_404
end end
def create_oauth_application(application)
oauth_application_params = {
name: params[:application],
redirect_uri: application.callback_url,
scopes: 'api read_user openid',
owner: current_user
}
Applications::CreateService.new(current_user, oauth_application_params).execute
end
end end
...@@ -10,8 +10,8 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -10,8 +10,8 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update] before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update]
before_action :check_issues_available! before_action :check_issues_available!
before_action :issue, except: [:index, :new, :create, :bulk_update] before_action :issue, except: [:index, :calendar, :new, :create, :bulk_update]
before_action :set_issuables_index, only: [:index] before_action :set_issuables_index, only: [:index, :calendar]
# Allow write(create) issue # Allow write(create) issue
before_action :authorize_create_issue!, only: [:new, :create] before_action :authorize_create_issue!, only: [:new, :create]
...@@ -39,6 +39,17 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -39,6 +39,17 @@ class Projects::IssuesController < Projects::ApplicationController
end end
end end
def calendar
@issues = @issuables
.non_archived
.with_due_date
.limit(100)
respond_to do |format|
format.ics { response.headers['Content-Disposition'] = 'inline' }
end
end
def new def new
params[:issue] ||= ActionController::Parameters.new( params[:issue] ||= ActionController::Parameters.new(
assignee_ids: "" assignee_ids: ""
......
...@@ -296,14 +296,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo ...@@ -296,14 +296,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
elsif @merge_request.actual_head_pipeline.success? elsif @merge_request.actual_head_pipeline.success?
# This can be triggered when a user clicks the auto merge button while # This can be triggered when a user clicks the auto merge button while
# the tests finish at about the same time # the tests finish at about the same time
@merge_request.merge_async(current_user.id, params) @merge_request.merge_async(current_user.id, merge_params)
:success :success
else else
:failed :failed
end end
else else
@merge_request.merge_async(current_user.id, params) @merge_request.merge_async(current_user.id, merge_params)
:success :success
end end
......
...@@ -75,6 +75,8 @@ class IssuesFinder < IssuableFinder ...@@ -75,6 +75,8 @@ class IssuesFinder < IssuableFinder
items = items.due_between(Date.today.beginning_of_week, Date.today.end_of_week) items = items.due_between(Date.today.beginning_of_week, Date.today.end_of_week)
elsif filter_by_due_this_month? elsif filter_by_due_this_month?
items = items.due_between(Date.today.beginning_of_month, Date.today.end_of_month) items = items.due_between(Date.today.beginning_of_month, Date.today.end_of_month)
elsif filter_by_due_next_month_and_previous_two_weeks?
items = items.due_between(Date.today - 2.weeks, (Date.today + 1.month).end_of_month)
end end
end end
...@@ -97,6 +99,10 @@ class IssuesFinder < IssuableFinder ...@@ -97,6 +99,10 @@ class IssuesFinder < IssuableFinder
due_date? && params[:due_date] == Issue::DueThisMonth.name due_date? && params[:due_date] == Issue::DueThisMonth.name
end end
def filter_by_due_next_month_and_previous_two_weeks?
due_date? && params[:due_date] == Issue::DueNextMonthAndPreviousTwoWeeks.name
end
def due_date? def due_date?
params[:due_date].present? params[:due_date].present?
end end
......
module CalendarHelper
def calendar_url_options
{ format: :ics,
feed_token: current_user.try(:feed_token),
due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
sort: 'closest_future_date' }
end
end
module RssHelper module RssHelper
def rss_url_options def rss_url_options
{ format: :atom, rss_token: current_user.try(:rss_token) } { format: :atom, feed_token: current_user.try(:feed_token) }
end end
end end
...@@ -360,17 +360,6 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -360,17 +360,6 @@ class ApplicationSetting < ActiveRecord::Base
Array(read_attribute(:repository_storages)) Array(read_attribute(:repository_storages))
end end
# DEPRECATED
# repository_storage is still required in the API. Remove in 9.0
# Still used in API v3
def repository_storage
repository_storages.first
end
def repository_storage=(value)
self.repository_storages = [value]
end
def default_project_visibility=(level) def default_project_visibility=(level)
super(Gitlab::VisibilityLevel.level_value(level)) super(Gitlab::VisibilityLevel.level_value(level))
end end
......
module Clusters
module Applications
class Jupyter < ActiveRecord::Base
VERSION = '0.0.1'.freeze
self.table_name = 'clusters_applications_jupyter'
include ::Clusters::Concerns::ApplicationCore
include ::Clusters::Concerns::ApplicationStatus
include ::Clusters::Concerns::ApplicationData
belongs_to :oauth_application, class_name: 'Doorkeeper::Application'
default_value_for :version, VERSION
def set_initial_status
return unless not_installable?
if cluster&.application_ingress_installed? && cluster.application_ingress.external_ip
self.status = 'installable'
end
end
def chart
"#{name}/jupyterhub"
end
def repository
'https://jupyterhub.github.io/helm-chart/'
end
def values
content_values.to_yaml
end
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(
name,
chart: chart,
values: values,
repository: repository
)
end
def callback_url
"http://#{hostname}/hub/oauth_callback"
end
private
def specification
{
"ingress" => {
"hosts" => [hostname]
},
"hub" => {
"extraEnv" => {
"GITLAB_HOST" => gitlab_url
},
"cookieSecret" => cookie_secret
},
"proxy" => {
"secretToken" => secret_token
},
"auth" => {
"gitlab" => {
"clientId" => oauth_application.uid,
"clientSecret" => oauth_application.secret,
"callbackUrl" => callback_url
}
}
}
end
def gitlab_url
Gitlab.config.gitlab.url
end
def content_values
YAML.load_file(chart_values_file).deep_merge!(specification)
end
def secret_token
@secret_token ||= SecureRandom.hex(32)
end
def cookie_secret
@cookie_secret ||= SecureRandom.hex(32)
end
end
end
end
...@@ -8,7 +8,8 @@ module Clusters ...@@ -8,7 +8,8 @@ module Clusters
Applications::Helm.application_name => Applications::Helm, Applications::Helm.application_name => Applications::Helm,
Applications::Ingress.application_name => Applications::Ingress, Applications::Ingress.application_name => Applications::Ingress,
Applications::Prometheus.application_name => Applications::Prometheus, Applications::Prometheus.application_name => Applications::Prometheus,
Applications::Runner.application_name => Applications::Runner Applications::Runner.application_name => Applications::Runner,
Applications::Jupyter.application_name => Applications::Jupyter
}.freeze }.freeze
DEFAULT_ENVIRONMENT = '*'.freeze DEFAULT_ENVIRONMENT = '*'.freeze
...@@ -26,6 +27,7 @@ module Clusters ...@@ -26,6 +27,7 @@ module Clusters
has_one :application_ingress, class_name: 'Clusters::Applications::Ingress' has_one :application_ingress, class_name: 'Clusters::Applications::Ingress'
has_one :application_prometheus, class_name: 'Clusters::Applications::Prometheus' has_one :application_prometheus, class_name: 'Clusters::Applications::Prometheus'
has_one :application_runner, class_name: 'Clusters::Applications::Runner' has_one :application_runner, class_name: 'Clusters::Applications::Runner'
has_one :application_jupyter, class_name: 'Clusters::Applications::Jupyter'
accepts_nested_attributes_for :provider_gcp, update_only: true accepts_nested_attributes_for :provider_gcp, update_only: true
accepts_nested_attributes_for :platform_kubernetes, update_only: true accepts_nested_attributes_for :platform_kubernetes, update_only: true
...@@ -39,6 +41,7 @@ module Clusters ...@@ -39,6 +41,7 @@ module Clusters
delegate :active?, to: :platform_kubernetes, prefix: true, allow_nil: true delegate :active?, to: :platform_kubernetes, prefix: true, allow_nil: true
delegate :installed?, to: :application_helm, prefix: true, allow_nil: true delegate :installed?, to: :application_helm, prefix: true, allow_nil: true
delegate :installed?, to: :application_ingress, prefix: true, allow_nil: true
enum platform_type: { enum platform_type: {
kubernetes: 1 kubernetes: 1
...@@ -74,7 +77,8 @@ module Clusters ...@@ -74,7 +77,8 @@ module Clusters
application_helm || build_application_helm, application_helm || build_application_helm,
application_ingress || build_application_ingress, application_ingress || build_application_ingress,
application_prometheus || build_application_prometheus, application_prometheus || build_application_prometheus,
application_runner || build_application_runner application_runner || build_application_runner,
application_jupyter || build_application_jupyter
] ]
end end
......
...@@ -97,8 +97,6 @@ module Issuable ...@@ -97,8 +97,6 @@ module Issuable
strip_attributes :title strip_attributes :title
after_save :ensure_metrics, unless: :imported?
# We want to use optimistic lock for cases when only title or description are involved # We want to use optimistic lock for cases when only title or description are involved
# http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html # http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html
def locking_enabled? def locking_enabled?
......
# Makes api V3 compatible with old project features permissions methods # Makes api V4 compatible with old project features permissions methods
# #
# After migrating issues_enabled merge_requests_enabled builds_enabled snippets_enabled and wiki_enabled # After migrating issues_enabled merge_requests_enabled builds_enabled snippets_enabled and wiki_enabled
# fields to a new table "project_features", support for the old fields is still needed in the API. # fields to a new table "project_features", support for the old fields is still needed in the API.
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment