Merge branch 'master' into ide-list-merge-requests
This commit is contained in:
commit
9ffb60acd8
|
|
@ -8,8 +8,6 @@ engines:
|
|||
languages:
|
||||
- ruby
|
||||
- javascript
|
||||
exclude_paths:
|
||||
- "lib/api/v3/*"
|
||||
ratings:
|
||||
paths:
|
||||
- Gemfile.lock
|
||||
|
|
|
|||
56
.eslintrc
56
.eslintrc
|
|
@ -1,56 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
---
|
||||
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
|
|||
/tags
|
||||
/tmp/*
|
||||
/vendor/bundle/*
|
||||
/vendor/gitaly-ruby
|
||||
/builds*
|
||||
/shared/*
|
||||
/.gitlab_workhorse_secret
|
||||
|
|
|
|||
|
|
@ -591,7 +591,7 @@ ee_compat_check:
|
|||
except:
|
||||
- master
|
||||
- tags
|
||||
- /^[\d-]+-stable(-ee)?/
|
||||
- /[\d-]+-stable(-ee)?/
|
||||
- /^security-/
|
||||
- branches@gitlab-org/gitlab-ee
|
||||
- branches@gitlab/gitlab-ee
|
||||
|
|
|
|||
|
|
@ -173,7 +173,6 @@ Lint/UriEscapeUnescape:
|
|||
- 'spec/requests/api/files_spec.rb'
|
||||
- 'spec/requests/api/internal_spec.rb'
|
||||
- 'spec/requests/api/issues_spec.rb'
|
||||
- 'spec/requests/api/v3/issues_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
|
||||
|
|
@ -333,8 +332,6 @@ RSpec/ScatteredSetup:
|
|||
- 'spec/lib/gitlab/bitbucket_import/importer_spec.rb'
|
||||
- 'spec/lib/gitlab/git/env_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'
|
||||
|
||||
# Offense count: 1
|
||||
|
|
@ -618,7 +615,6 @@ Style/OrAssignment:
|
|||
Exclude:
|
||||
- 'app/models/concerns/token_authenticatable.rb'
|
||||
- 'lib/api/commit_statuses.rb'
|
||||
- 'lib/api/v3/members.rb'
|
||||
- 'lib/gitlab/project_transfer.rb'
|
||||
|
||||
# Offense count: 50
|
||||
|
|
@ -781,7 +777,6 @@ Style/TernaryParentheses:
|
|||
- 'app/finders/projects_finder.rb'
|
||||
- 'app/helpers/namespaces_helper.rb'
|
||||
- 'features/support/capybara.rb'
|
||||
- 'lib/api/v3/projects.rb'
|
||||
- 'lib/gitlab/ci/build/artifacts/metadata/entry.rb'
|
||||
- 'spec/requests/api/pipeline_schedules_spec.rb'
|
||||
- 'spec/support/capybara.rb'
|
||||
|
|
|
|||
14
CHANGELOG.md
14
CHANGELOG.md
|
|
@ -2,6 +2,20 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
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)
|
||||
|
||||
### Security (3 changes)
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
0.103.0
|
||||
0.104.0
|
||||
|
|
|
|||
9
Gemfile
9
Gemfile
|
|
@ -28,7 +28,7 @@ gem 'mysql2', '~> 0.4.10', group: :mysql
|
|||
gem 'pg', '~> 0.18.2', group: :postgres
|
||||
|
||||
gem 'rugged', '~> 0.27'
|
||||
gem 'grape-route-helpers', '~> 2.1.0'
|
||||
gem 'grape-path-helpers', '~> 1.0'
|
||||
|
||||
gem 'faraday', '~> 0.12'
|
||||
|
||||
|
|
@ -144,6 +144,9 @@ gem 'truncato', '~> 0.7.9'
|
|||
gem 'bootstrap_form', '~> 2.7.0'
|
||||
gem 'nokogiri', '~> 1.8.2'
|
||||
|
||||
# Calendar rendering
|
||||
gem 'icalendar'
|
||||
|
||||
# Diffs
|
||||
gem 'diffy', '~> 3.1.0'
|
||||
|
||||
|
|
@ -219,7 +222,7 @@ gem 'asana', '~> 0.6.0'
|
|||
gem 'ruby-fogbugz', '~> 0.2.1'
|
||||
|
||||
# Kubernetes integration
|
||||
gem 'kubeclient', '~> 3.0'
|
||||
gem 'kubeclient', '~> 3.1.0'
|
||||
|
||||
# Sanitize user input
|
||||
gem 'sanitize', '~> 2.0'
|
||||
|
|
@ -320,7 +323,7 @@ group :development, :test do
|
|||
gem 'pry-byebug', '~> 3.4.1', platform: :mri
|
||||
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 'database_cleaner', '~> 1.5.0'
|
||||
|
|
|
|||
26
Gemfile.lock
26
Gemfile.lock
|
|
@ -168,7 +168,7 @@ GEM
|
|||
diff-lcs (1.3)
|
||||
diffy (3.1.0)
|
||||
docile (1.1.5)
|
||||
domain_name (0.5.20170404)
|
||||
domain_name (0.5.20180417)
|
||||
unf (>= 0.0.5, < 1.0.0)
|
||||
doorkeeper (4.3.2)
|
||||
railties (>= 4.2)
|
||||
|
|
@ -348,7 +348,7 @@ GEM
|
|||
signet (~> 0.7)
|
||||
gpgme (2.0.13)
|
||||
mini_portile2 (~> 2.1)
|
||||
grape (1.0.2)
|
||||
grape (1.0.3)
|
||||
activesupport
|
||||
builder
|
||||
mustermann-grape (~> 1.0.0)
|
||||
|
|
@ -358,10 +358,10 @@ GEM
|
|||
grape-entity (0.7.1)
|
||||
activesupport (>= 4.0)
|
||||
multi_json (>= 1.3.2)
|
||||
grape-route-helpers (2.1.0)
|
||||
activesupport
|
||||
grape (>= 0.16.0)
|
||||
rake
|
||||
grape-path-helpers (1.0.1)
|
||||
activesupport (~> 4)
|
||||
grape (~> 1.0)
|
||||
rake (~> 12)
|
||||
grape_logging (1.7.0)
|
||||
grape
|
||||
grpc (1.11.0)
|
||||
|
|
@ -410,6 +410,7 @@ GEM
|
|||
httpclient (2.8.3)
|
||||
i18n (0.9.5)
|
||||
concurrent-ruby (~> 1.0)
|
||||
icalendar (2.4.1)
|
||||
ice_nine (0.11.2)
|
||||
influxdb (0.2.3)
|
||||
cause
|
||||
|
|
@ -446,9 +447,9 @@ GEM
|
|||
knapsack (1.16.0)
|
||||
rake
|
||||
timecop (>= 0.1.0)
|
||||
kubeclient (3.0.0)
|
||||
kubeclient (3.1.0)
|
||||
http (~> 2.2.2)
|
||||
recursive-open-struct (~> 1.0.4)
|
||||
recursive-open-struct (~> 1.0, >= 1.0.4)
|
||||
rest-client (~> 2.0)
|
||||
launchy (2.4.3)
|
||||
addressable (~> 2.3)
|
||||
|
|
@ -698,7 +699,7 @@ GEM
|
|||
re2 (1.1.1)
|
||||
recaptcha (3.0.0)
|
||||
json
|
||||
recursive-open-struct (1.0.5)
|
||||
recursive-open-struct (1.1.0)
|
||||
redcarpet (3.4.0)
|
||||
redis (3.3.5)
|
||||
redis-actionpack (5.0.2)
|
||||
|
|
@ -977,7 +978,7 @@ DEPENDENCIES
|
|||
asciidoctor-plantuml (= 0.0.8)
|
||||
asset_sync (~> 2.4)
|
||||
attr_encrypted (~> 3.1.0)
|
||||
awesome_print (~> 1.8.0)
|
||||
awesome_print
|
||||
babosa (~> 1.0.2)
|
||||
base32 (~> 0.3.0)
|
||||
batch-loader (~> 1.2.1)
|
||||
|
|
@ -1049,7 +1050,7 @@ DEPENDENCIES
|
|||
gpgme
|
||||
grape (~> 1.0)
|
||||
grape-entity (~> 0.7.1)
|
||||
grape-route-helpers (~> 2.1.0)
|
||||
grape-path-helpers (~> 1.0)
|
||||
grape_logging (~> 1.7)
|
||||
grpc (~> 1.11.0)
|
||||
haml_lint (~> 0.26.0)
|
||||
|
|
@ -1060,6 +1061,7 @@ DEPENDENCIES
|
|||
html-pipeline (~> 2.7.1)
|
||||
html2text
|
||||
httparty (~> 0.13.3)
|
||||
icalendar
|
||||
influxdb (~> 0.2)
|
||||
jira-ruby (~> 1.4)
|
||||
jquery-atwho-rails (~> 1.3.2)
|
||||
|
|
@ -1067,7 +1069,7 @@ DEPENDENCIES
|
|||
jwt (~> 1.5.6)
|
||||
kaminari (~> 1.0)
|
||||
knapsack (~> 1.16)
|
||||
kubeclient (~> 3.0)
|
||||
kubeclient (~> 3.1.0)
|
||||
letter_opener_web (~> 1.3.0)
|
||||
license_finder (~> 3.1)
|
||||
licensee (~> 8.9)
|
||||
|
|
|
|||
|
|
@ -25,8 +25,6 @@ const Api = {
|
|||
commitPipelinesPath: '/:project_id/commit/:sha/pipelines',
|
||||
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
|
||||
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) {
|
||||
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
|
||||
|
|
@ -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) {
|
||||
let urlRoot = '';
|
||||
if (gon.relative_url_root != null) {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/* eslint-disable comma-dangle, space-before-function-paren, one-var */
|
||||
|
||||
import $ from 'jquery';
|
||||
import Sortable from 'vendor/Sortable';
|
||||
import Sortable from 'sortablejs';
|
||||
import Vue from 'vue';
|
||||
import AccessorUtilities from '../../lib/utils/accessor';
|
||||
import boardList from './board_list.vue';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script>
|
||||
import Sortable from 'vendor/Sortable';
|
||||
import Sortable from 'sortablejs';
|
||||
import boardNewIssue from './board_new_issue.vue';
|
||||
import boardCard from './board_card.vue';
|
||||
import eventHub from '../eventhub';
|
||||
|
|
|
|||
|
|
@ -41,10 +41,10 @@ gl.issueBoards.ModalEmptyState = Vue.extend({
|
|||
template: `
|
||||
<section class="empty-state">
|
||||
<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>
|
||||
</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">
|
||||
<h4>{{ contents.title }}</h4>
|
||||
<p v-html="contents.content"></p>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
/* global ListIssue */
|
||||
|
||||
import Vue from 'vue';
|
||||
import bp from '../../../breakpoints';
|
||||
import ModalStore from '../../stores/modal_store';
|
||||
|
|
@ -56,8 +54,11 @@ gl.issueBoards.ModalList = Vue.extend({
|
|||
scrollHandler() {
|
||||
const currentPage = Math.floor(this.issues.length / this.perPage);
|
||||
|
||||
if ((this.scrollTop() > this.scrollHeight() - 100) && !this.loadingNewPage
|
||||
&& currentPage === this.page) {
|
||||
if (
|
||||
this.scrollTop() > this.scrollHeight() - 100 &&
|
||||
!this.loadingNewPage &&
|
||||
currentPage === this.page
|
||||
) {
|
||||
this.loadingNewPage = true;
|
||||
this.page += 1;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,71 +1,69 @@
|
|||
<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';
|
||||
import _ from 'underscore';
|
||||
import eventHub from '../eventhub';
|
||||
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
|
||||
import Api from '../../api';
|
||||
|
||||
export default {
|
||||
name: 'BoardProjectSelect',
|
||||
components: {
|
||||
loadingIcon,
|
||||
export default {
|
||||
name: 'BoardProjectSelect',
|
||||
components: {
|
||||
loadingIcon,
|
||||
},
|
||||
props: {
|
||||
groupId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
default: 0,
|
||||
},
|
||||
props: {
|
||||
groupId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
default: 0,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
selectedProject: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
selectedProjectName() {
|
||||
return this.selectedProject.name || 'Select a project';
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
$(this.$refs.projectsDropdown).glDropdown({
|
||||
filterable: true,
|
||||
filterRemote: true,
|
||||
search: {
|
||||
fields: ['name_with_namespace'],
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
loading: true,
|
||||
selectedProject: {},
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
selectedProjectName() {
|
||||
return this.selectedProject.name || 'Select a project';
|
||||
clicked: ({ $el, e }) => {
|
||||
e.preventDefault();
|
||||
this.selectedProject = {
|
||||
id: $el.data('project-id'),
|
||||
name: $el.data('project-name'),
|
||||
};
|
||||
eventHub.$emit('setSelectedProject', this.selectedProject);
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
$(this.$refs.projectsDropdown).glDropdown({
|
||||
filterable: true,
|
||||
filterRemote: true,
|
||||
search: {
|
||||
fields: ['name_with_namespace'],
|
||||
},
|
||||
clicked: ({ $el, e }) => {
|
||||
e.preventDefault();
|
||||
this.selectedProject = {
|
||||
id: $el.data('project-id'),
|
||||
name: $el.data('project-name'),
|
||||
};
|
||||
eventHub.$emit('setSelectedProject', this.selectedProject);
|
||||
},
|
||||
selectable: true,
|
||||
data: (term, callback) => {
|
||||
this.loading = true;
|
||||
return Api.groupProjects(this.groupId, term, (projects) => {
|
||||
this.loading = false;
|
||||
callback(projects);
|
||||
});
|
||||
},
|
||||
renderRow(project) {
|
||||
return `
|
||||
selectable: true,
|
||||
data: (term, callback) => {
|
||||
this.loading = true;
|
||||
return Api.groupProjects(this.groupId, term, projects => {
|
||||
this.loading = false;
|
||||
callback(projects);
|
||||
});
|
||||
},
|
||||
renderRow(project) {
|
||||
return `
|
||||
<li>
|
||||
<a href='#' class='dropdown-menu-link' data-project-id="${project.id}" data-project-name="${project.name}">
|
||||
${_.escape(project.name)}
|
||||
</a>
|
||||
</li>
|
||||
`;
|
||||
},
|
||||
text: project => project.name,
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
text: project => project.name,
|
||||
});
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ export default class Clusters {
|
|||
installHelmPath,
|
||||
installIngressPath,
|
||||
installRunnerPath,
|
||||
installJupyterPath,
|
||||
installPrometheusPath,
|
||||
managePrometheusPath,
|
||||
clusterStatus,
|
||||
|
|
@ -51,6 +52,7 @@ export default class Clusters {
|
|||
installIngressEndpoint: installIngressPath,
|
||||
installRunnerEndpoint: installRunnerPath,
|
||||
installPrometheusEndpoint: installPrometheusPath,
|
||||
installJupyterEndpoint: installJupyterPath,
|
||||
});
|
||||
|
||||
this.installApplication = this.installApplication.bind(this);
|
||||
|
|
@ -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, 'requestReason', null);
|
||||
|
||||
this.service.installApplication(appId)
|
||||
this.service.installApplication(appId, data.params)
|
||||
.then(() => {
|
||||
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS);
|
||||
})
|
||||
|
|
|
|||
|
|
@ -52,6 +52,11 @@
|
|||
type: String,
|
||||
required: false,
|
||||
},
|
||||
installApplicationRequestParams: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
rowJsClass() {
|
||||
|
|
@ -109,7 +114,10 @@
|
|||
},
|
||||
methods: {
|
||||
installClicked() {
|
||||
eventHub.$emit('installApplication', this.id);
|
||||
eventHub.$emit('installApplication', {
|
||||
id: this.id,
|
||||
params: this.installApplicationRequestParams,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -121,6 +121,12 @@ export default {
|
|||
false,
|
||||
);
|
||||
},
|
||||
jupyterInstalled() {
|
||||
return this.applications.jupyter.status === APPLICATION_INSTALLED;
|
||||
},
|
||||
jupyterHostname() {
|
||||
return this.applications.jupyter.hostname;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
@ -278,11 +284,67 @@ export default {
|
|||
applications to production.`) }}
|
||||
</div>
|
||||
</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`
|
||||
min-height for this block and uncomment `application_spec` tests
|
||||
-->
|
||||
<!-- Add GitLab Runner row, all other plumbing is complete -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -11,3 +11,4 @@ export const REQUEST_LOADING = 'request-loading';
|
|||
export const REQUEST_SUCCESS = 'request-success';
|
||||
export const REQUEST_FAILURE = 'request-failure';
|
||||
export const INGRESS = 'ingress';
|
||||
export const JUPYTER = 'jupyter';
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ export default class ClusterService {
|
|||
ingress: this.options.installIngressEndpoint,
|
||||
runner: this.options.installRunnerEndpoint,
|
||||
prometheus: this.options.installPrometheusEndpoint,
|
||||
jupyter: this.options.installJupyterEndpoint,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -15,8 +16,8 @@ export default class ClusterService {
|
|||
return axios.get(this.options.endpoint);
|
||||
}
|
||||
|
||||
installApplication(appId) {
|
||||
return axios.post(this.appInstallEndpointMap[appId]);
|
||||
installApplication(appId, params) {
|
||||
return axios.post(this.appInstallEndpointMap[appId], params);
|
||||
}
|
||||
|
||||
static updateCluster(endpoint, data) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { s__ } from '../../locale';
|
||||
import { INGRESS } from '../constants';
|
||||
import { INGRESS, JUPYTER } from '../constants';
|
||||
|
||||
export default class ClusterStore {
|
||||
constructor() {
|
||||
|
|
@ -38,6 +38,14 @@ export default class ClusterStore {
|
|||
requestStatus: 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 {
|
|||
|
||||
if (appId === INGRESS) {
|
||||
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 {
|
|||
</div>
|
||||
<form
|
||||
v-if="!isCompact"
|
||||
class="form-horizontal"
|
||||
@submit.prevent.stop="commitChanges"
|
||||
ref="formEl"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,41 @@
|
|||
<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';
|
|||
import IdeStatusBar from './ide_status_bar.vue';
|
||||
import RepoEditor from './repo_editor.vue';
|
||||
import FindFile from './file_finder/index.vue';
|
||||
import RightPane from './panes/right.vue';
|
||||
|
||||
const originalStopCallback = Mousetrap.stopCallback;
|
||||
|
||||
|
|
@ -16,6 +17,7 @@ export default {
|
|||
IdeStatusBar,
|
||||
RepoEditor,
|
||||
FindFile,
|
||||
RightPane,
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
|
|
@ -25,6 +27,7 @@ export default {
|
|||
'currentMergeRequestId',
|
||||
'fileFindVisible',
|
||||
'emptyStateSvgPath',
|
||||
'currentProjectId',
|
||||
]),
|
||||
...mapGetters(['activeFile', 'hasChanges']),
|
||||
},
|
||||
|
|
@ -122,6 +125,9 @@ export default {
|
|||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<right-pane
|
||||
v-if="currentProjectId"
|
||||
/>
|
||||
</div>
|
||||
<ide-status-bar :file="activeFile"/>
|
||||
</article>
|
||||
|
|
|
|||
|
|
@ -1,84 +0,0 @@
|
|||
<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 {
|
|||
computed: {
|
||||
...mapState(['currentBranchId', 'currentProjectId']),
|
||||
...mapGetters(['currentProject', 'lastCommit']),
|
||||
...mapState('pipelines', ['latestPipeline']),
|
||||
},
|
||||
watch: {
|
||||
lastCommit() {
|
||||
|
|
@ -51,14 +52,14 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['pipelinePoll', 'stopPipelinePolling']),
|
||||
...mapActions('pipelines', ['fetchLatestPipeline', 'stopPipelinePolling']),
|
||||
startTimer() {
|
||||
this.intervalId = setInterval(() => {
|
||||
this.commitAgeUpdate();
|
||||
}, 1000);
|
||||
},
|
||||
initPipelinePolling() {
|
||||
this.pipelinePoll();
|
||||
this.fetchLatestPipeline();
|
||||
this.isPollingInitialized = true;
|
||||
},
|
||||
commitAgeUpdate() {
|
||||
|
|
@ -81,18 +82,18 @@ export default {
|
|||
>
|
||||
<span
|
||||
class="ide-status-pipeline"
|
||||
v-if="lastCommit.pipeline && lastCommit.pipeline.details"
|
||||
v-if="latestPipeline && latestPipeline.details"
|
||||
>
|
||||
<ci-icon
|
||||
:status="lastCommit.pipeline.details.status"
|
||||
:status="latestPipeline.details.status"
|
||||
v-tooltip
|
||||
:title="lastCommit.pipeline.details.status.text"
|
||||
:title="latestPipeline.details.status.text"
|
||||
/>
|
||||
Pipeline
|
||||
<a
|
||||
class="monospace"
|
||||
:href="lastCommit.pipeline.details.status.details_path">#{{ lastCommit.pipeline.id }}</a>
|
||||
{{ lastCommit.pipeline.details.status.text }}
|
||||
:href="latestPipeline.details.status.details_path">#{{ latestPipeline.id }}</a>
|
||||
{{ latestPipeline.details.status.text }}
|
||||
for
|
||||
</span>
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,46 @@
|
|||
<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>
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<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>
|
||||
|
|
@ -0,0 +1,108 @@
|
|||
<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>
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
<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>
|
||||
|
|
@ -0,0 +1,146 @@
|
|||
<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
|
|||
import { activityBarViews, viewerTypes } from '../constants';
|
||||
import monacoLoader from '../monaco_loader';
|
||||
import Editor from '../lib/editor';
|
||||
import IdeFileButtons from './ide_file_buttons.vue';
|
||||
import ExternalLink from './external_link.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ContentViewer,
|
||||
IdeFileButtons,
|
||||
ExternalLink,
|
||||
},
|
||||
props: {
|
||||
file: {
|
||||
|
|
@ -224,7 +224,7 @@ export default {
|
|||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<ide-file-buttons
|
||||
<external-link
|
||||
:file="file"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,3 +20,7 @@ export const viewerTypes = {
|
|||
edit: 'editor',
|
||||
diff: 'diff',
|
||||
};
|
||||
|
||||
export const rightSidebarViews = {
|
||||
pipelines: 'pipelines-list',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ router.beforeEach((to, from, next) => {
|
|||
.then(() => {
|
||||
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];
|
||||
|
||||
if (branchId) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import Vue from 'vue';
|
||||
import { mapActions } from 'vuex';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import ide from './components/ide.vue';
|
||||
import store from './stores';
|
||||
|
|
@ -17,11 +18,18 @@ export function initIde(el) {
|
|||
ide,
|
||||
},
|
||||
created() {
|
||||
this.$store.dispatch('setEmptyStateSvgs', {
|
||||
this.setEmptyStateSvgs({
|
||||
emptyStateSvgPath: el.dataset.emptyStateSvgPath,
|
||||
noChangesStateSvgPath: el.dataset.noChangesStateSvgPath,
|
||||
committedStateSvgPath: el.dataset.committedStateSvgPath,
|
||||
pipelinesEmptyStateSvgPath: el.dataset.pipelinesEmptyStateSvgPath,
|
||||
});
|
||||
this.setLinks({
|
||||
ciHelpPagePath: el.dataset.ciHelpPagePath,
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setEmptyStateSvgs', 'setLinks']),
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('ide');
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
/* global monaco */
|
||||
import Disposable from './disposable';
|
||||
import eventHub from '../../eventhub';
|
||||
|
||||
|
|
|
|||
|
|
@ -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/file';
|
||||
export * from './actions/project';
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
};
|
||||
|
||||
export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => {
|
||||
export const getRawFileData = ({ state, commit }, { path, baseSha }) => {
|
||||
const file = state.entries[path];
|
||||
return new Promise((resolve, reject) => {
|
||||
service
|
||||
|
|
@ -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 });
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import service from '../../services';
|
|||
import * as types from '../mutation_types';
|
||||
|
||||
export const getMergeRequestData = (
|
||||
{ commit, state, dispatch },
|
||||
{ commit, state },
|
||||
{ projectId, mergeRequestId, force = false } = {},
|
||||
) =>
|
||||
new Promise((resolve, reject) => {
|
||||
|
|
@ -32,7 +32,7 @@ export const getMergeRequestData = (
|
|||
});
|
||||
|
||||
export const getMergeRequestChanges = (
|
||||
{ commit, state, dispatch },
|
||||
{ commit, state },
|
||||
{ projectId, mergeRequestId, force = false } = {},
|
||||
) =>
|
||||
new Promise((resolve, reject) => {
|
||||
|
|
@ -58,7 +58,7 @@ export const getMergeRequestChanges = (
|
|||
});
|
||||
|
||||
export const getMergeRequestVersions = (
|
||||
{ commit, state, dispatch },
|
||||
{ commit, state },
|
||||
{ projectId, mergeRequestId, force = false } = {},
|
||||
) =>
|
||||
new Promise((resolve, reject) => {
|
||||
|
|
|
|||
|
|
@ -1,16 +1,9 @@
|
|||
import Visibility from 'visibilityjs';
|
||||
import flash from '~/flash';
|
||||
import { __ } from '~/locale';
|
||||
import service from '../../services';
|
||||
import * as types from '../mutation_types';
|
||||
import Poll from '../../../lib/utils/poll';
|
||||
|
||||
let eTagPoll;
|
||||
|
||||
export const getProjectData = (
|
||||
{ commit, state, dispatch },
|
||||
{ namespace, projectId, force = false } = {},
|
||||
) =>
|
||||
export const getProjectData = ({ commit, state }, { namespace, projectId, force = false } = {}) =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (!state.projects[`${namespace}/${projectId}`] || force) {
|
||||
commit(types.TOGGLE_LOADING, { entry: state });
|
||||
|
|
@ -40,10 +33,7 @@ export const getProjectData = (
|
|||
}
|
||||
});
|
||||
|
||||
export const getBranchData = (
|
||||
{ commit, state, dispatch },
|
||||
{ projectId, branchId, force = false } = {},
|
||||
) =>
|
||||
export const getBranchData = ({ commit, state }, { projectId, branchId, force = false } = {}) =>
|
||||
new Promise((resolve, reject) => {
|
||||
if (
|
||||
typeof state.projects[`${projectId}`] === 'undefined' ||
|
||||
|
|
@ -78,7 +68,7 @@ export const getBranchData = (
|
|||
}
|
||||
});
|
||||
|
||||
export const refreshLastCommitData = ({ commit, state, dispatch }, { projectId, branchId } = {}) =>
|
||||
export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {}) =>
|
||||
service
|
||||
.getBranchData(projectId, branchId)
|
||||
.then(({ data }) => {
|
||||
|
|
@ -91,61 +81,3 @@ export const refreshLastCommitData = ({ commit, state, dispatch }, { projectId,
|
|||
.catch(() => {
|
||||
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';
|
|||
import { findEntry } from '../utils';
|
||||
import FilesDecoratorWorker from '../workers/files_decorator_worker';
|
||||
|
||||
export const toggleTreeOpen = ({ commit, dispatch }, path) => {
|
||||
export const toggleTreeOpen = ({ commit }, path) => {
|
||||
commit(types.TOGGLE_TREE_OPEN, path);
|
||||
};
|
||||
|
||||
|
|
@ -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;
|
||||
|
||||
service
|
||||
|
|
@ -49,7 +49,7 @@ export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = s
|
|||
.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) => {
|
||||
if (!state.trees[`${projectId}/${branchId}`]) {
|
||||
const selectedProject = state.projects[projectId];
|
||||
|
|
|
|||
|
|
@ -10,14 +10,17 @@ import mergeRequests from './modules/merge_requests';
|
|||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: state(),
|
||||
actions,
|
||||
mutations,
|
||||
getters,
|
||||
modules: {
|
||||
commit: commitModule,
|
||||
pipelines,
|
||||
mergeRequests,
|
||||
},
|
||||
});
|
||||
export const createStore = () =>
|
||||
new Vuex.Store({
|
||||
state: state(),
|
||||
actions,
|
||||
mutations,
|
||||
getters,
|
||||
modules: {
|
||||
commit: commitModule,
|
||||
pipelines,
|
||||
mergeRequests,
|
||||
},
|
||||
});
|
||||
|
||||
export default createStore();
|
||||
|
|
|
|||
|
|
@ -31,9 +31,9 @@ export const setLastCommitMessage = ({ rootState, commit }, data) => {
|
|||
const currentProject = rootState.projects[rootState.currentProjectId];
|
||||
const commitStats = data.stats
|
||||
? sprintf(__('with %{additions} additions, %{deletions} deletions.'), {
|
||||
additions: data.stats.additions, // eslint-disable-line indent
|
||||
deletions: data.stats.deletions, // eslint-disable-line indent
|
||||
}) // eslint-disable-line indent
|
||||
additions: data.stats.additions, // eslint-disable-line indent-legacy
|
||||
deletions: data.stats.deletions, // eslint-disable-line indent-legacy
|
||||
}) // eslint-disable-line indent-legacy
|
||||
: '';
|
||||
const commitMsg = sprintf(
|
||||
__('Your changes have been committed. Commit %{commitId} %{commitStats}'),
|
||||
|
|
@ -74,10 +74,7 @@ export const checkCommitStatus = ({ rootState }) =>
|
|||
),
|
||||
);
|
||||
|
||||
export const updateFilesAfterCommit = (
|
||||
{ commit, dispatch, state, rootState, rootGetters },
|
||||
{ data },
|
||||
) => {
|
||||
export const updateFilesAfterCommit = ({ commit, dispatch, rootState }, { data }) => {
|
||||
const selectedProject = rootState.projects[rootState.currentProjectId];
|
||||
const lastCommit = {
|
||||
commit_path: `${selectedProject.web_url}/commit/${data.id}`,
|
||||
|
|
|
|||
|
|
@ -1,49 +1,80 @@
|
|||
import Visibility from 'visibilityjs';
|
||||
import axios from 'axios';
|
||||
import { __ } from '../../../../locale';
|
||||
import Api from '../../../../api';
|
||||
import flash from '../../../../flash';
|
||||
import Poll from '../../../../lib/utils/poll';
|
||||
import service from '../../../services';
|
||||
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 receiveLatestPipelineError = ({ commit }) => {
|
||||
export const receiveLatestPipelineError = ({ commit, dispatch }) => {
|
||||
flash(__('There was an error loading latest pipeline'));
|
||||
commit(types.RECEIVE_LASTEST_PIPELINE_ERROR);
|
||||
dispatch('stopPipelinePolling');
|
||||
};
|
||||
export const receiveLatestPipelineSuccess = ({ commit }, pipeline) =>
|
||||
commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, pipeline);
|
||||
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 fetchLatestPipeline = ({ dispatch, rootGetters }) => {
|
||||
if (eTagPoll) return;
|
||||
|
||||
export const fetchLatestPipeline = ({ dispatch, rootState }, sha) => {
|
||||
dispatch('requestLatestPipeline');
|
||||
|
||||
return Api.pipelines(rootState.currentProjectId, { sha, per_page: '1' })
|
||||
.then(({ data }) => {
|
||||
dispatch('receiveLatestPipelineSuccess', data.pop());
|
||||
})
|
||||
.catch(() => dispatch('receiveLatestPipelineError'));
|
||||
eTagPoll = new Poll({
|
||||
resource: service,
|
||||
method: 'lastCommitPipelines',
|
||||
data: { getters: rootGetters },
|
||||
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 receiveJobsError = ({ commit }) => {
|
||||
export const requestJobs = ({ commit }, id) => commit(types.REQUEST_JOBS, id);
|
||||
export const receiveJobsError = ({ commit }, id) => {
|
||||
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') => {
|
||||
dispatch('requestJobs');
|
||||
export const fetchJobs = ({ dispatch }, stage) => {
|
||||
dispatch('requestJobs', stage.id);
|
||||
|
||||
Api.pipelineJobs(rootState.currentProjectId, state.latestPipeline.id, {
|
||||
page,
|
||||
})
|
||||
.then(({ data, headers }) => {
|
||||
const nextPage = headers && headers['x-next-page'];
|
||||
|
||||
dispatch('receiveJobsSuccess', data);
|
||||
|
||||
if (nextPage) {
|
||||
dispatch('fetchJobs', nextPage);
|
||||
}
|
||||
})
|
||||
.catch(() => dispatch('receiveJobsError'));
|
||||
axios
|
||||
.get(stage.dropdownPath)
|
||||
.then(({ data }) => dispatch('receiveJobsSuccess', { id: stage.id, data }))
|
||||
.catch(() => dispatch('receiveJobsError', stage.id));
|
||||
};
|
||||
|
||||
export const toggleStageCollapsed = ({ commit }, stageId) =>
|
||||
commit(types.TOGGLE_STAGE_COLLAPSE, stageId);
|
||||
|
||||
export default () => {};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,4 @@
|
|||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const states = {
|
||||
failed: 'failed',
|
||||
};
|
||||
|
|
@ -1,7 +1,22 @@
|
|||
import { states } from './constants';
|
||||
|
||||
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(
|
||||
(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
|
|||
export const REQUEST_JOBS = 'REQUEST_JOBS';
|
||||
export const RECEIVE_JOBS_ERROR = 'RECEIVE_JOBS_ERROR';
|
||||
export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS';
|
||||
|
||||
export const TOGGLE_STAGE_COLLAPSE = 'TOGGLE_STAGE_COLLAPSE';
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
import * as types from './mutation_types';
|
||||
import { normalizeJob } from './utils';
|
||||
|
||||
export default {
|
||||
[types.REQUEST_LATEST_PIPELINE](state) {
|
||||
|
|
@ -14,40 +15,52 @@ export default {
|
|||
if (pipeline) {
|
||||
state.latestPipeline = {
|
||||
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) {
|
||||
state.isLoadingJobs = true;
|
||||
[types.REQUEST_JOBS](state, id) {
|
||||
state.stages = state.stages.map(stage => ({
|
||||
...stage,
|
||||
isLoading: stage.id === id ? true : stage.isLoading,
|
||||
}));
|
||||
},
|
||||
[types.RECEIVE_JOBS_ERROR](state) {
|
||||
state.isLoadingJobs = false;
|
||||
[types.RECEIVE_JOBS_ERROR](state, id) {
|
||||
state.stages = state.stages.map(stage => ({
|
||||
...stage,
|
||||
isLoading: stage.id === id ? false : stage.isLoading,
|
||||
}));
|
||||
},
|
||||
[types.RECEIVE_JOBS_SUCCESS](state, jobs) {
|
||||
state.isLoadingJobs = false;
|
||||
|
||||
state.stages = jobs.reduce((acc, job) => {
|
||||
let stage = acc.find(s => s.title === job.stage);
|
||||
|
||||
if (!stage) {
|
||||
stage = {
|
||||
title: job.stage,
|
||||
jobs: [],
|
||||
};
|
||||
|
||||
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);
|
||||
[types.RECEIVE_JOBS_SUCCESS](state, { id, data }) {
|
||||
state.stages = state.stages.map(stage => ({
|
||||
...stage,
|
||||
isLoading: stage.id === id ? false : stage.isLoading,
|
||||
jobs: stage.id === id ? data.latest_statuses.map(normalizeJob) : stage.jobs,
|
||||
}));
|
||||
},
|
||||
[types.TOGGLE_STAGE_COLLAPSE](state, id) {
|
||||
state.stages = state.stages.map(stage => ({
|
||||
...stage,
|
||||
isCollapsed: stage.id === id ? !stage.isCollapsed : stage.isCollapsed,
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
export default () => ({
|
||||
isLoadingPipeline: false,
|
||||
isLoadingPipeline: true,
|
||||
isLoadingJobs: false,
|
||||
latestPipeline: null,
|
||||
stages: [],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,7 @@
|
|||
// 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';
|
|||
export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
|
||||
export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS';
|
||||
export const SET_EMPTY_STATE_SVGS = 'SET_EMPTY_STATE_SVGS';
|
||||
export const SET_LINKS = 'SET_LINKS';
|
||||
|
||||
// Project Mutation Types
|
||||
export const SET_PROJECT = 'SET_PROJECT';
|
||||
|
|
@ -23,7 +24,6 @@ export const SET_BRANCH = 'SET_BRANCH';
|
|||
export const SET_BRANCH_COMMIT = 'SET_BRANCH_COMMIT';
|
||||
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
|
||||
export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
|
||||
export const SET_LAST_COMMIT_PIPELINE = 'SET_LAST_COMMIT_PIPELINE';
|
||||
|
||||
// Tree mutation types
|
||||
export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
|
||||
|
|
@ -66,3 +66,5 @@ export const UPDATE_ACTIVITY_BAR_VIEW = 'UPDATE_ACTIVITY_BAR_VIEW';
|
|||
export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
|
||||
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
|
||||
export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL';
|
||||
|
||||
export const SET_RIGHT_PANE = 'SET_RIGHT_PANE';
|
||||
|
|
|
|||
|
|
@ -114,12 +114,13 @@ export default {
|
|||
},
|
||||
[types.SET_EMPTY_STATE_SVGS](
|
||||
state,
|
||||
{ emptyStateSvgPath, noChangesStateSvgPath, committedStateSvgPath },
|
||||
{ emptyStateSvgPath, noChangesStateSvgPath, committedStateSvgPath, pipelinesEmptyStateSvgPath },
|
||||
) {
|
||||
Object.assign(state, {
|
||||
emptyStateSvgPath,
|
||||
noChangesStateSvgPath,
|
||||
committedStateSvgPath,
|
||||
pipelinesEmptyStateSvgPath,
|
||||
});
|
||||
},
|
||||
[types.TOGGLE_FILE_FINDER](state, fileFindVisible) {
|
||||
|
|
@ -148,6 +149,14 @@ export default {
|
|||
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,
|
||||
...mergeRequestMutation,
|
||||
...fileMutations,
|
||||
|
|
|
|||
|
|
@ -14,10 +14,6 @@ export default {
|
|||
treeId: `${projectPath}/${branchName}`,
|
||||
active: true,
|
||||
workingReference: '',
|
||||
commit: {
|
||||
...branch.commit,
|
||||
pipeline: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -32,9 +28,4 @@ export default {
|
|||
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 () => ({
|
|||
currentActivityView: activityBarViews.edit,
|
||||
unusedSeal: true,
|
||||
fileFindVisible: false,
|
||||
rightPane: null,
|
||||
links: {},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ export default class IssuableForm {
|
|||
}
|
||||
|
||||
this.initAutosave();
|
||||
this.form.on('submit:success', this.handleSubmit);
|
||||
this.form.on('submit', this.handleSubmit);
|
||||
this.form.on('click', '.btn-cancel', this.resetAutosave);
|
||||
this.initWip();
|
||||
|
||||
|
|
|
|||
|
|
@ -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 support sticky, then we allow the browser to handle it, if not
|
||||
then we use a polyfill
|
||||
**/
|
||||
*/
|
||||
if (this.$topBar.css('position') !== 'static') return;
|
||||
|
||||
StickyFill.add(this.$topBar);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
/* global Build */
|
||||
|
||||
import Visibility from 'visibilityjs';
|
||||
import Flash from '../flash';
|
||||
import Poll from '../lib/utils/poll';
|
||||
|
|
@ -50,7 +48,8 @@ export default class JobMediator {
|
|||
}
|
||||
|
||||
getJob() {
|
||||
return this.service.getJob()
|
||||
return this.service
|
||||
.getJob()
|
||||
.then(response => this.successCallback(response))
|
||||
.catch(() => this.errorCallback());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
/* 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 Sortable from 'vendor/Sortable';
|
||||
import Sortable from 'sortablejs';
|
||||
|
||||
import flash from './flash';
|
||||
import axios from './lib/utils/axios_utils';
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ delete window.translations;
|
|||
Translates `text`
|
||||
@param text The text to be translated
|
||||
@returns {String} The translated text
|
||||
**/
|
||||
*/
|
||||
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 count Number to decide which translation to use (eg. 2)
|
||||
@returns {String} Translated text with the number replaced (eg. '2 days')
|
||||
**/
|
||||
*/
|
||||
const ngettext = (text, pluralText, count) => {
|
||||
const translated = locale.ngettext(text, pluralText, count).replace(/%d/g, count).split('|');
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ const ngettext = (text, pluralText, count) => {
|
|||
(eg. 'Context')
|
||||
@param key Is the dynamic variable you want to be translated
|
||||
@returns {String} Translated context based text
|
||||
**/
|
||||
*/
|
||||
const pgettext = (keyOrContext, key) => {
|
||||
const normalizedKey = key ? `${keyOrContext}|${key}` : keyOrContext;
|
||||
const translated = gettext(normalizedKey).split('|');
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import _ from 'underscore';
|
|||
|
||||
@see https://ruby-doc.org/core-2.3.3/Kernel.html#method-i-sprintf
|
||||
@see https://gitlab.com/gitlab-org/gitlab-ce/issues/37992
|
||||
**/
|
||||
*/
|
||||
export default (input, parameters, escapeParameters = true) => {
|
||||
let output = input;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 support sticky, then we allow the browser to handle it, if not
|
||||
then we default back to Bootstraps affix
|
||||
**/
|
||||
*/
|
||||
if ($tabs.css('position') !== 'static') return;
|
||||
|
||||
const $diffTabs = $('#diff-notes-app');
|
||||
|
|
|
|||
|
|
@ -12,20 +12,13 @@ import { isInViewport, scrollToElement } from '../../lib/utils/common_utils';
|
|||
|
||||
let eTagPoll;
|
||||
|
||||
export const setNotesData = ({ commit }, data) =>
|
||||
commit(types.SET_NOTES_DATA, data);
|
||||
export const setNoteableData = ({ commit }, data) =>
|
||||
commit(types.SET_NOTEABLE_DATA, data);
|
||||
export const setUserData = ({ commit }, data) =>
|
||||
commit(types.SET_USER_DATA, data);
|
||||
export const setLastFetchedAt = ({ commit }, 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 setNotesData = ({ commit }, data) => commit(types.SET_NOTES_DATA, data);
|
||||
export const setNoteableData = ({ commit }, data) => commit(types.SET_NOTEABLE_DATA, data);
|
||||
export const setUserData = ({ commit }, data) => commit(types.SET_USER_DATA, data);
|
||||
export const setLastFetchedAt = ({ commit }, 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) =>
|
||||
service
|
||||
|
|
@ -69,20 +62,14 @@ export const createNewNote = ({ commit }, { endpoint, data }) =>
|
|||
return res;
|
||||
});
|
||||
|
||||
export const removePlaceholderNotes = ({ commit }) =>
|
||||
commit(types.REMOVE_PLACEHOLDER_NOTES);
|
||||
export const removePlaceholderNotes = ({ commit }) => commit(types.REMOVE_PLACEHOLDER_NOTES);
|
||||
|
||||
export const toggleResolveNote = (
|
||||
{ commit },
|
||||
{ endpoint, isResolved, discussion },
|
||||
) =>
|
||||
export const toggleResolveNote = ({ commit }, { endpoint, isResolved, discussion }) =>
|
||||
service
|
||||
.toggleResolveNote(endpoint, isResolved)
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
const mutationType = discussion
|
||||
? types.UPDATE_DISCUSSION
|
||||
: types.UPDATE_NOTE;
|
||||
const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE;
|
||||
|
||||
commit(mutationType, res);
|
||||
});
|
||||
|
|
@ -114,7 +101,7 @@ export const reopenIssue = ({ commit, dispatch, state }) => {
|
|||
export const toggleStateButtonLoading = ({ commit }, 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', {
|
||||
detail: {
|
||||
data,
|
||||
|
|
@ -179,10 +166,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
|
|||
|
||||
loadAwardsHandler()
|
||||
.then(awardsHandler => {
|
||||
awardsHandler.addAwardToEmojiBar(
|
||||
votesBlock,
|
||||
commandsChanges.emoji_award,
|
||||
);
|
||||
awardsHandler.addAwardToEmojiBar(votesBlock, commandsChanges.emoji_award);
|
||||
awardsHandler.scrollToAwards();
|
||||
})
|
||||
.catch(() => {
|
||||
|
|
@ -194,10 +178,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
|
|||
});
|
||||
}
|
||||
|
||||
if (
|
||||
commandsChanges.spend_time != null ||
|
||||
commandsChanges.time_estimate != null
|
||||
) {
|
||||
if (commandsChanges.spend_time != null || commandsChanges.time_estimate != null) {
|
||||
sidebarTimeTrackingEventHub.$emit('timeTrackingUpdated', res);
|
||||
}
|
||||
}
|
||||
|
|
@ -218,14 +199,8 @@ const pollSuccessCallBack = (resp, commit, state, getters) => {
|
|||
resp.notes.forEach(note => {
|
||||
if (notesById[note.id]) {
|
||||
commit(types.UPDATE_NOTE, note);
|
||||
} else if (
|
||||
note.type === constants.DISCUSSION_NOTE ||
|
||||
note.type === constants.DIFF_NOTE
|
||||
) {
|
||||
const discussion = utils.findNoteObjectById(
|
||||
state.notes,
|
||||
note.discussion_id,
|
||||
);
|
||||
} else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) {
|
||||
const discussion = utils.findNoteObjectById(state.notes, note.discussion_id);
|
||||
|
||||
if (discussion) {
|
||||
commit(types.ADD_NEW_REPLY_TO_DISCUSSION, note);
|
||||
|
|
@ -249,11 +224,8 @@ export const poll = ({ commit, state, getters }) => {
|
|||
method: 'poll',
|
||||
data: state,
|
||||
successCallback: resp =>
|
||||
resp
|
||||
.json()
|
||||
.then(data => pollSuccessCallBack(data, commit, state, getters)),
|
||||
errorCallback: () =>
|
||||
Flash('Something went wrong while fetching latest comments.'),
|
||||
resp.json().then(data => pollSuccessCallBack(data, commit, state, getters)),
|
||||
errorCallback: () => Flash('Something went wrong while fetching latest comments.'),
|
||||
});
|
||||
|
||||
if (!Visibility.hidden()) {
|
||||
|
|
@ -292,14 +264,11 @@ export const fetchData = ({ commit, state, getters }) => {
|
|||
.catch(() => Flash('Something went wrong while fetching latest comments.'));
|
||||
};
|
||||
|
||||
export const toggleAward = (
|
||||
{ commit, state, getters, dispatch },
|
||||
{ awardName, noteId },
|
||||
) => {
|
||||
export const toggleAward = ({ commit, getters }, { awardName, 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;
|
||||
|
||||
return service
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export default {
|
|||
ref="form"
|
||||
:action="deleteWikiUrl"
|
||||
method="post"
|
||||
class="form-horizontal js-requires-input"
|
||||
class="js-requires-input"
|
||||
>
|
||||
<input
|
||||
ref="method"
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import $ from 'jquery';
|
|||
*
|
||||
* Toggling this checkbox adds/removes a `remember_me` parameter to the
|
||||
* login buttons' href, which is passed on to the omniauth callback.
|
||||
**/
|
||||
*/
|
||||
|
||||
export default class OAuthRememberMe {
|
||||
constructor(opts = {}) {
|
||||
|
|
|
|||
|
|
@ -79,12 +79,13 @@ export default {
|
|||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="ci-job-dropdown-container dropdown">
|
||||
<div class="ci-job-dropdown-container dropdown dropright">
|
||||
<button
|
||||
v-tooltip
|
||||
type="button"
|
||||
data-toggle="dropdown"
|
||||
data-container="body"
|
||||
data-boundary="viewport"
|
||||
class="dropdown-menu-toggle build-content"
|
||||
:title="tooltipText"
|
||||
>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ const IGNORE_URLS = [
|
|||
/extensions\//i,
|
||||
/^chrome:\/\//i,
|
||||
// Other plugins
|
||||
/127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb
|
||||
/127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb
|
||||
/webappstoolbarba\.texthelp\.com\//i,
|
||||
/metrics\.itunes\.apple\.com\.edgesuite\.net\//i,
|
||||
];
|
||||
|
|
|
|||
|
|
@ -7,9 +7,10 @@ Vue.use(VueResource);
|
|||
export const fetchRepos = ({ commit, state }) => {
|
||||
commit(types.TOGGLE_MAIN_LOADING);
|
||||
|
||||
return Vue.http.get(state.endpoint)
|
||||
return Vue.http
|
||||
.get(state.endpoint)
|
||||
.then(res => res.json())
|
||||
.then((response) => {
|
||||
.then(response => {
|
||||
commit(types.TOGGLE_MAIN_LOADING);
|
||||
commit(types.SET_REPOS_LIST, response);
|
||||
});
|
||||
|
|
@ -18,19 +19,20 @@ export const fetchRepos = ({ commit, state }) => {
|
|||
export const fetchList = ({ commit }, { repo, page }) => {
|
||||
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
|
||||
|
||||
return Vue.http.get(repo.tagsPath, { params: { page } })
|
||||
.then((response) => {
|
||||
const headers = response.headers;
|
||||
return Vue.http.get(repo.tagsPath, { params: { page } }).then(response => {
|
||||
const headers = response.headers;
|
||||
|
||||
return response.json().then((resp) => {
|
||||
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
|
||||
commit(types.SET_REGISTRY_LIST, { repo, resp, headers });
|
||||
});
|
||||
return response.json().then(resp => {
|
||||
commit(types.TOGGLE_REGISTRY_LIST_LOADING, repo);
|
||||
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);
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
export const deleteRegistry = ({ commit }, image) => Vue.http.delete(image.destroyPath);
|
||||
|
||||
export const setMainEndpoint = ({ commit }, data) => commit(types.SET_MAIN_ENDPOINT, data);
|
||||
|
|
|
|||
|
|
@ -168,8 +168,8 @@
|
|||
<a
|
||||
:href="mr.mergeCommitPath"
|
||||
class="commit-sha js-mr-merged-commit-sha"
|
||||
v-text="mr.shortMergeCommitSha"
|
||||
>
|
||||
{{ mr.shortMergeCommitSha }}
|
||||
</a>
|
||||
<clipboard-button
|
||||
:title="__('Copy commit SHA to clipboard')"
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import Icon from '../../vue_shared/components/icon.vue';
|
|||
* - Jobs show view header
|
||||
* - Jobs show view sidebar
|
||||
*/
|
||||
const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
|
|
@ -31,17 +33,36 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 16,
|
||||
validator(value) {
|
||||
return validSizes.includes(value);
|
||||
},
|
||||
},
|
||||
borderless: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
cssClass() {
|
||||
const status = this.status.group;
|
||||
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>
|
||||
<template>
|
||||
<span :class="cssClass">
|
||||
<icon :name="status.icon" />
|
||||
<icon
|
||||
:name="icon"
|
||||
:size="size"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
<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>
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
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';
|
|||
|
||||
export default {
|
||||
bind(el) {
|
||||
$(el).tooltip();
|
||||
$(el).tooltip({
|
||||
trigger: 'hover',
|
||||
});
|
||||
},
|
||||
|
||||
componentUpdated(el) {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ export default (Vue) => {
|
|||
|
||||
@param text The text to be translated
|
||||
@returns {String} The translated text
|
||||
**/
|
||||
*/
|
||||
__,
|
||||
/**
|
||||
Translate the text with a number
|
||||
|
|
@ -24,7 +24,7 @@ export default (Vue) => {
|
|||
@param pluralText Plural text to translate (eg. '%d days')
|
||||
@param count Number to decide which translation to use (eg. 2)
|
||||
@returns {String} Translated text with the number replaced (eg. '2 days')
|
||||
**/
|
||||
*/
|
||||
n__,
|
||||
/**
|
||||
Translate context based text
|
||||
|
|
@ -36,7 +36,7 @@ export default (Vue) => {
|
|||
(eg. 'Context')
|
||||
@param key Is the dynamic variable you want to be translated
|
||||
@returns {String} Translated context based text
|
||||
**/
|
||||
*/
|
||||
s__,
|
||||
sprintf,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -87,7 +87,8 @@ table {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.dropdown-toggle::after {
|
||||
.dropdown-toggle::after,
|
||||
.dropright .dropdown-menu-toggle::after {
|
||||
// Remove bootstrap's dropdown caret
|
||||
display: none;
|
||||
}
|
||||
|
|
@ -148,8 +149,14 @@ table {
|
|||
}
|
||||
}
|
||||
|
||||
.nav-tabs .nav-link {
|
||||
border: 0;
|
||||
.nav-tabs {
|
||||
.nav-link {
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
pre code {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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 @@
|
|||
color: $color-800;
|
||||
}
|
||||
|
||||
.nav-links li a.active {
|
||||
border-bottom: 2px solid $color-500;
|
||||
.nav-links li {
|
||||
&.active a,
|
||||
a.active {
|
||||
border-bottom: 2px solid $color-500;
|
||||
|
||||
.badge.badge-pill {
|
||||
font-weight: $gl-font-weight-bold;
|
||||
.badge.badge-pill {
|
||||
font-weight: $gl-font-weight-bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -189,6 +192,10 @@
|
|||
&.active {
|
||||
color: $color-700;
|
||||
box-shadow: inset 3px 0 $color-700;
|
||||
|
||||
&.is-right {
|
||||
box-shadow: inset -3px 0 $color-700;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,14 +19,23 @@
|
|||
width: auto;
|
||||
display: inline-block;
|
||||
overflow-x: auto;
|
||||
border-left: 0;
|
||||
border-right: 0;
|
||||
border-bottom: 0;
|
||||
border: 0;
|
||||
border-color: $md-area-border;
|
||||
|
||||
@supports(width: fit-content) {
|
||||
display: block;
|
||||
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 {
|
|||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
.modal-full {
|
||||
width: 98%;
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
background-color: $black-transparent;
|
||||
z-index: 2100;
|
||||
|
|
|
|||
|
|
@ -31,14 +31,15 @@
|
|||
color: $black;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
&.active a,
|
||||
a.active {
|
||||
color: $black;
|
||||
font-weight: $gl-font-weight-bold;
|
||||
|
||||
.badge.badge-pill {
|
||||
color: $black;
|
||||
font-weight: $gl-font-weight-bold;
|
||||
|
||||
.badge.badge-pill {
|
||||
color: $black;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -49,26 +49,11 @@
|
|||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.snippet-embed-input {
|
||||
height: 35px;
|
||||
}
|
||||
|
||||
.embed-snippet {
|
||||
padding-right: 0;
|
||||
padding-top: $gl-padding;
|
||||
|
||||
.form-control {
|
||||
cursor: auto;
|
||||
width: 101%;
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
.embed-toggle-list li button {
|
||||
padding: 8px 40px;
|
||||
}
|
||||
|
||||
.embed-toggle,
|
||||
.snippet-clipboard-btn {
|
||||
height: 35px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -373,6 +373,7 @@ $dropdown-chevron-size: 10px;
|
|||
$dropdown-toggle-active-border-color: darken($border-color, 14%);
|
||||
$dropdown-item-hover-bg: $gray-darker;
|
||||
$dropdown-fade-mask-height: 32px;
|
||||
$dropdown-member-form-control-width: 163px;
|
||||
|
||||
/*
|
||||
* Filtered Search
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
.cluster-applications-table {
|
||||
// Wait for the Vue to kick-in and render the applications block
|
||||
min-height: 400px;
|
||||
min-height: 628px;
|
||||
}
|
||||
|
||||
.clusters-dropdown-menu {
|
||||
|
|
|
|||
|
|
@ -36,13 +36,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.form-horizontal {
|
||||
margin-top: 20px;
|
||||
.form-group {
|
||||
margin-bottom: 0;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
display: -webkit-flex;
|
||||
display: flex;
|
||||
margin-top: 3px;
|
||||
@include media-breakpoint-down(sm) {
|
||||
display: block;
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -62,10 +61,15 @@
|
|||
}
|
||||
|
||||
.member-form-control {
|
||||
@include media-breakpoint-down(xs) {
|
||||
padding-bottom: 5px;
|
||||
@include media-breakpoint-down(sm) {
|
||||
width: $dropdown-member-form-control-width;
|
||||
margin-left: 0;
|
||||
padding-bottom: 5px;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
margin-right: 0;
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -207,10 +211,6 @@
|
|||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.form-horizontal ~ .btn {
|
||||
margin-right: 0;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
display: block;
|
||||
|
||||
|
|
@ -220,6 +220,12 @@
|
|||
display: block;
|
||||
}
|
||||
|
||||
.controls > .btn:last-child {
|
||||
margin-left: 5px;
|
||||
margin-right: 5px;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
width: 100%;
|
||||
}
|
||||
|
|
@ -232,10 +238,6 @@
|
|||
.member-controls {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.form-horizontal {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -259,10 +261,6 @@
|
|||
margin-top: 0;
|
||||
}
|
||||
|
||||
.form-horizontal {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.member-form-control {
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -909,6 +909,16 @@
|
|||
width: 1px;
|
||||
background: $white-light;
|
||||
}
|
||||
|
||||
&.is-right {
|
||||
padding-right: $gl-padding;
|
||||
padding-left: $gl-padding + 1px;
|
||||
|
||||
&::after {
|
||||
right: auto;
|
||||
left: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1121,3 +1131,112 @@
|
|||
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
|
|||
end
|
||||
|
||||
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
|
||||
|
||||
def render_404
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
render file: Rails.root.join("public", "404"), layout: false, status: "404"
|
||||
end
|
||||
format.html { render "errors/not_found", layout: "errors", status: 404 }
|
||||
# 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.any { head :not_found }
|
||||
|
|
|
|||
|
|
@ -17,10 +17,23 @@ module IssuesAction
|
|||
end
|
||||
# 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
|
||||
|
||||
def finder_type
|
||||
(super if defined?(super)) ||
|
||||
(IssuesFinder if action_name == 'issues')
|
||||
(IssuesFinder if %w(issues issues_calendar).include?(action_name))
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
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
|
|||
redirect_to profile_personal_access_tokens_path
|
||||
end
|
||||
|
||||
def reset_rss_token
|
||||
def reset_feed_token
|
||||
Users::UpdateService.new(current_user, user: @user).execute! do |user|
|
||||
user.reset_rss_token!
|
||||
user.reset_feed_token!
|
||||
end
|
||||
|
||||
flash[:notice] = "RSS token was successfully reset"
|
||||
flash[:notice] = 'Feed token was successfully reset'
|
||||
|
||||
redirect_to profile_personal_access_tokens_path
|
||||
end
|
||||
|
|
|
|||
|
|
@ -5,7 +5,17 @@ class Projects::Clusters::ApplicationsController < Projects::ApplicationControll
|
|||
before_action :authorize_create_cluster!, only: [: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)
|
||||
|
||||
|
|
@ -23,4 +33,15 @@ class Projects::Clusters::ApplicationsController < Projects::ApplicationControll
|
|||
def application_class
|
||||
@application_class ||= Clusters::Cluster::APPLICATIONS[params[:application]] || render_404
|
||||
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
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
|
||||
before_action :whitelist_query_limiting, only: [:create, :create_merge_request, :move, :bulk_update]
|
||||
before_action :check_issues_available!
|
||||
before_action :issue, except: [:index, :new, :create, :bulk_update]
|
||||
before_action :set_issuables_index, only: [:index]
|
||||
before_action :issue, except: [:index, :calendar, :new, :create, :bulk_update]
|
||||
before_action :set_issuables_index, only: [:index, :calendar]
|
||||
|
||||
# Allow write(create) issue
|
||||
before_action :authorize_create_issue!, only: [:new, :create]
|
||||
|
|
@ -39,6 +39,17 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
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
|
||||
params[:issue] ||= ActionController::Parameters.new(
|
||||
assignee_ids: ""
|
||||
|
|
|
|||
|
|
@ -296,14 +296,14 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
|
|||
elsif @merge_request.actual_head_pipeline.success?
|
||||
# This can be triggered when a user clicks the auto merge button while
|
||||
# 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
|
||||
else
|
||||
:failed
|
||||
end
|
||||
else
|
||||
@merge_request.merge_async(current_user.id, params)
|
||||
@merge_request.merge_async(current_user.id, merge_params)
|
||||
|
||||
:success
|
||||
end
|
||||
|
|
|
|||
|
|
@ -75,6 +75,8 @@ class IssuesFinder < IssuableFinder
|
|||
items = items.due_between(Date.today.beginning_of_week, Date.today.end_of_week)
|
||||
elsif filter_by_due_this_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
|
||||
|
||||
|
|
@ -97,6 +99,10 @@ class IssuesFinder < IssuableFinder
|
|||
due_date? && params[:due_date] == Issue::DueThisMonth.name
|
||||
end
|
||||
|
||||
def filter_by_due_next_month_and_previous_two_weeks?
|
||||
due_date? && params[:due_date] == Issue::DueNextMonthAndPreviousTwoWeeks.name
|
||||
end
|
||||
|
||||
def due_date?
|
||||
params[:due_date].present?
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
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
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
module RssHelper
|
||||
def rss_url_options
|
||||
{ format: :atom, rss_token: current_user.try(:rss_token) }
|
||||
{ format: :atom, feed_token: current_user.try(:feed_token) }
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -360,17 +360,6 @@ class ApplicationSetting < ActiveRecord::Base
|
|||
Array(read_attribute(:repository_storages))
|
||||
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)
|
||||
super(Gitlab::VisibilityLevel.level_value(level))
|
||||
end
|
||||
|
|
|
|||
|
|
@ -0,0 +1,92 @@
|
|||
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
|
|||
Applications::Helm.application_name => Applications::Helm,
|
||||
Applications::Ingress.application_name => Applications::Ingress,
|
||||
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
|
||||
DEFAULT_ENVIRONMENT = '*'.freeze
|
||||
|
||||
|
|
@ -26,6 +27,7 @@ module Clusters
|
|||
has_one :application_ingress, class_name: 'Clusters::Applications::Ingress'
|
||||
has_one :application_prometheus, class_name: 'Clusters::Applications::Prometheus'
|
||||
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 :platform_kubernetes, update_only: true
|
||||
|
|
@ -39,6 +41,7 @@ module Clusters
|
|||
|
||||
delegate :active?, to: :platform_kubernetes, 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: {
|
||||
kubernetes: 1
|
||||
|
|
@ -74,7 +77,8 @@ module Clusters
|
|||
application_helm || build_application_helm,
|
||||
application_ingress || build_application_ingress,
|
||||
application_prometheus || build_application_prometheus,
|
||||
application_runner || build_application_runner
|
||||
application_runner || build_application_runner,
|
||||
application_jupyter || build_application_jupyter
|
||||
]
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -97,8 +97,6 @@ module Issuable
|
|||
|
||||
strip_attributes :title
|
||||
|
||||
after_save :ensure_metrics, unless: :imported?
|
||||
|
||||
# We want to use optimistic lock for cases when only title or description are involved
|
||||
# http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html
|
||||
def locking_enabled?
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# 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
|
||||
# fields to a new table "project_features", support for the old fields is still needed in the API.
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue