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

This commit is contained in:
Phil Hughes 2018-06-01 12:12:18 +01:00
commit 9ffb60acd8
No known key found for this signature in database
GPG Key ID: 32245528C52E0F9F
481 changed files with 6116 additions and 20347 deletions

View File

@ -8,8 +8,6 @@ engines:
languages:
- ruby
- javascript
exclude_paths:
- "lib/api/v3/*"
ratings:
paths:
- Gemfile.lock

View File

@ -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"
}
]
}
}

77
.eslintrc.yml Normal file
View File

@ -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

1
.gitignore vendored
View File

@ -64,6 +64,7 @@ eslint-report.html
/tags
/tmp/*
/vendor/bundle/*
/vendor/gitaly-ruby
/builds*
/shared/*
/.gitlab_workhorse_secret

View File

@ -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

View File

@ -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'

View File

@ -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)

View File

@ -1 +1 @@
0.103.0
0.104.0

View File

@ -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'

View File

@ -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)

View File

@ -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) {

View File

@ -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';

View File

@ -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';

View File

@ -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>

View File

@ -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;
}

View File

@ -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>

View File

@ -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);
})

View File

@ -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,
});
},
},
};

View File

@ -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>

View File

@ -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';

View File

@ -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) {

View File

@ -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`
: '');
}
});
}

View File

@ -126,7 +126,6 @@ export default {
</div>
<form
v-if="!isCompact"
class="form-horizontal"
@submit.prevent.stop="commitChanges"
ref="formEl"
>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -20,3 +20,7 @@ export const viewerTypes = {
edit: 'editor',
diff: 'diff',
};
export const rightSidebarViews = {
pipelines: 'pipelines-list',
};

View File

@ -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) {

View File

@ -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');

View File

@ -1,4 +1,3 @@
/* global monaco */
import Disposable from './disposable';
import eventHub from '../../eventhub';

View File

@ -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';

View File

@ -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 });
};

View File

@ -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) => {

View File

@ -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();
};

View File

@ -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];

View File

@ -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();

View File

@ -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}`,

View File

@ -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 () => {};

View File

@ -0,0 +1,4 @@
// eslint-disable-next-line import/prefer-default-export
export const states = {
failed: 'failed',
};

View File

@ -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 () => {};

View File

@ -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';

View File

@ -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,
}));
},
};

View File

@ -1,5 +1,5 @@
export default () => ({
isLoadingPipeline: false,
isLoadingPipeline: true,
isLoadingJobs: false,
latestPipeline: null,
stages: [],

View File

@ -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,
});

View File

@ -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';

View File

@ -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,

View File

@ -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,
});
},
};

View File

@ -23,4 +23,6 @@ export default () => ({
currentActivityView: activityBarViews.edit,
unusedSeal: true,
fileFindVisible: false,
rightPane: null,
links: {},
});

View File

@ -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();

View File

@ -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);

View File

@ -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());
}

View File

@ -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';

View File

@ -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('|');

View File

@ -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;

View File

@ -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');

View File

@ -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

View File

@ -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"

View File

@ -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 = {}) {

View File

@ -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"
>

View File

@ -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,
];

View File

@ -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);

View File

@ -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')"

View File

@ -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>

View File

@ -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>

View File

@ -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]);
},
};

View File

@ -2,7 +2,9 @@ import $ from 'jquery';
export default {
bind(el) {
$(el).tooltip();
$(el).tooltip({
trigger: 'hover',
});
},
componentUpdated(el) {

View File

@ -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,
},

View File

@ -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 {

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}
}
/*

View File

@ -74,12 +74,6 @@ body.modal-open {
}
}
@include media-breakpoint-up(lg) {
.modal-full {
width: 98%;
}
}
.modal {
background-color: $black-transparent;
z-index: 2100;

View File

@ -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;
}
}
}
}

View File

@ -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;
}
}

View File

@ -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

View File

@ -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 {

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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: ""

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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?

View File

@ -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