未验证 提交 9ffb60ac 编写于 作者: P Phil Hughes

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

......@@ -8,8 +8,6 @@ engines:
languages:
- ruby
- javascript
exclude_paths:
- "lib/api/v3/*"
ratings:
paths:
- Gemfile.lock
......
{
"env": {
"browser": true,
"es6": true
},
"extends": [
"airbnb-base",
"plugin:vue/recommended"
],
"globals": {
"__webpack_public_path__": true,
"gl": false,
"gon": false,
"localStorage": false
},
"parserOptions": {
"parser": "babel-eslint"
},
"plugins": [
"filenames",
"import",
"html",
"promise"
],
"settings": {
"html/html-extensions": [".html", ".html.raw"],
"import/resolver": {
"webpack": {
"config": "./config/webpack.config.js"
}
}
},
"rules": {
"filenames/match-regex": [2, "^[a-z0-9_]+$"],
"import/no-commonjs": "error",
"no-multiple-empty-lines": ["error", { "max": 1 }],
"promise/catch-or-return": "error",
"no-underscore-dangle": ["error", { "allow": ["__", "_links"] }],
"no-mixed-operators": 0,
"space-before-function-paren": 0,
"curly": 0,
"arrow-parens": 0,
"vue/html-self-closing": [
"error",
{
"html": {
"void": "always",
"normal": "never",
"component": "always"
},
"svg": "always",
"math": "always"
}
]
}
}
---
env:
browser: true
es6: true
extends:
- airbnb-base
- plugin:vue/recommended
globals:
__webpack_public_path__: true
gl: false
gon: false
localStorage: false
parserOptions:
parser: babel-eslint
plugins:
- filenames
- import
- html
- promise
settings:
html/html-extensions:
- ".html"
- ".html.raw"
import/resolver:
webpack:
config: "./config/webpack.config.js"
rules:
filenames/match-regex:
- error
- "^[a-z0-9_]+$"
import/no-commonjs: error
no-multiple-empty-lines:
- error
- max: 1
promise/catch-or-return: error
no-underscore-dangle:
- error
- allow:
- __
- _links
no-mixed-operators: off
vue/html-self-closing:
- error
- html:
void: always
normal: never
component: always
svg: always
math: always
## Conflicting rules with prettier:
space-before-function-paren: off
curly: off
arrow-parens: off
function-paren-newline: off
object-curly-newline: off
padded-blocks: off
# Disabled for now, to make the eslint 3 -> eslint 4 update smoother
## Indent rule. We are using the old for now: https://eslint.org/docs/user-guide/migrating-to-4.0.0#indent-rewrite
indent: off
indent-legacy:
- error
- 2
- SwitchCase: 1
VariableDeclarator: 1
outerIIFEBody: 1
FunctionDeclaration:
parameters: 1
body: 1
FunctionExpression:
parameters: 1
body: 1
## Destructuring: https://eslint.org/docs/rules/prefer-destructuring
prefer-destructuring: off
## no-restricted-globals: https://eslint.org/docs/rules/no-restricted-globals
no-restricted-globals: off
## no-multi-assign: https://eslint.org/docs/rules/no-multi-assign
no-multi-assign: off
......@@ -64,6 +64,7 @@ eslint-report.html
/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'
......
......@@ -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)
......
......@@ -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'
......
......@@ -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) {
......
/* 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';
......
<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>
......
/* 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;
}
......
<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,
},
props: {
groupId: {
type: Number,
required: true,
default: 0,
},
export default {
name: 'BoardProjectSelect',
components: {
loadingIcon,
},
props: {
groupId: {
type: Number,
required: true,
default: 0,
},
data() {
return {
loading: true,
selectedProject: {},
};
},
data() {
return {
loading: true,
selectedProject: {},
};
},
computed: {
selectedProjectName() {
return this.selectedProject.name || 'Select a project';
},
computed: {
selectedProjectName() {
return this.selectedProject.name || 'Select a project';
},
mounted() {
$(this.$refs.projectsDropdown).glDropdown({
filterable: true,
filterRemote: true,
search: {
fields: ['name_with_namespace'],
},
},
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 `
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 `
<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) {
......
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"
>
......
<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>
......
<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>
......
<script>
import Icon from '../../../vue_shared/components/icon.vue';
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
export default {
components: {
Icon,
CiIcon,
},
props: {
job: {
type: Object,
required: true,
},
},
computed: {
jobId() {
return `#${this.job.id}`;
},
},
};
</script>
<template>
<div class="ide-job-item">
<ci-icon
:status="job.status"
:borderless="true"
:size="24"
/>
<span class="prepend-left-8">
{{ job.name }}
<a
:href="job.path"
target="_blank"
class="ide-external-link"
>
{{ jobId }}
<icon
name="external-link"
:size="12"
/>
</a>
</span>
</div>
</template>
<script>
import { mapActions } from 'vuex';
import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
import Stage from './stage.vue';
export default {
components: {
LoadingIcon,
Stage,
},
props: {
stages: {
type: Array,
required: true,
},
loading: {
type: Boolean,
required: true,
},
},
methods: {
...mapActions('pipelines', ['fetchJobs', 'toggleStageCollapsed']),
},
};
</script>
<template>
<div>
<loading-icon
v-if="loading && !stages.length"
class="prepend-top-default"
size="2"
/>
<template v-else>
<stage
v-for="stage in stages"
:key="stage.id"
:stage="stage"
@fetch="fetchJobs"
@toggleCollapsed="toggleStageCollapsed"
/>
</template>
</div>
</template>
<script>
import tooltip from '../../../vue_shared/directives/tooltip';
import Icon from '../../../vue_shared/components/icon.vue';
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
import Item from './item.vue';
export default {
directives: {
tooltip,
},
components: {
Icon,
CiIcon,
LoadingIcon,
Item,
},
props: {
stage: {
type: Object,
required: true,
},
},
data() {
return {
showTooltip: false,
};
},
computed: {
collapseIcon() {
return this.stage.isCollapsed ? 'angle-left' : 'angle-down';
},
showLoadingIcon() {
return this.stage.isLoading && !this.stage.jobs.length;
},
jobsCount() {
return this.stage.jobs.length;
},
},
mounted() {
const { stageTitle } = this.$refs;
this.showTooltip = stageTitle.scrollWidth > stageTitle.offsetWidth;
this.$emit('fetch', this.stage);
},
methods: {
toggleCollapsed() {
this.$emit('toggleCollapsed', this.stage.id);
},
},
};
</script>
<template>
<div
class="ide-stage card prepend-top-default"
>
<div
class="card-header"
:class="{
'border-bottom-0': stage.isCollapsed
}"
@click="toggleCollapsed"
>
<ci-icon
:status="stage.status"
:size="24"
/>
<strong
v-tooltip="showTooltip"
:title="showTooltip ? stage.name : null"
data-container="body"
class="prepend-left-8 ide-stage-title"
ref="stageTitle"
>
{{ stage.name }}
</strong>
<div
v-if="!stage.isLoading || stage.jobs.length"
class="append-right-8 prepend-left-4"
>
<span class="badge badge-pill">
{{ jobsCount }}
</span>
</div>
<icon
:name="collapseIcon"
css-classes="ide-stage-collapse-icon"
/>
</div>
<div
class="card-body"
v-show="!stage.isCollapsed"
>
<loading-icon
v-if="showLoadingIcon"
/>
<template v-else>
<item
v-for="job in stage.jobs"
:key="job.id"
:job="job"
/>
</template>
</div>
</div>
</template>
<script>
import { mapActions, mapState } from 'vuex';
import tooltip from '../../../vue_shared/directives/tooltip';
import Icon from '../../../vue_shared/components/icon.vue';
import { rightSidebarViews } from '../../constants';
import PipelinesList from '../pipelines/list.vue';
export default {
directives: {
tooltip,
},
components: {
Icon,
PipelinesList,
},
computed: {
...mapState(['rightPane']),
},
methods: {
...mapActions(['setRightPane']),
clickTab(e, view) {
e.target.blur();
this.setRightPane(view);
},
},
rightSidebarViews,
};
</script>
<template>
<div
class="multi-file-commit-panel ide-right-sidebar"
>
<div
class="multi-file-commit-panel-inner"
v-if="rightPane"
>
<component :is="rightPane" />
</div>
<nav class="ide-activity-bar">
<ul class="list-unstyled">
<li>
<button
v-tooltip
data-container="body"
data-placement="left"
:title="__('Pipelines')"
class="ide-sidebar-link is-right"
:class="{
active: rightPane === $options.rightSidebarViews.pipelines
}"
type="button"
@click="clickTab($event, $options.rightSidebarViews.pipelines)"
>
<icon
:size="16"
name="pipeline"
/>
</button>
</li>
</ul>
</nav>
</div>
</template>
<script>
import { mapActions, mapGetters, mapState } from 'vuex';
import _ from 'underscore';
import { sprintf, __ } from '../../../locale';
import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
import Icon from '../../../vue_shared/components/icon.vue';
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
import Tabs from '../../../vue_shared/components/tabs/tabs';
import Tab from '../../../vue_shared/components/tabs/tab.vue';
import EmptyState from '../../../pipelines/components/empty_state.vue';
import JobsList from '../jobs/list.vue';
export default {
components: {
LoadingIcon,
Icon,
CiIcon,
Tabs,
Tab,
JobsList,
EmptyState,
},
computed: {
...mapState(['pipelinesEmptyStateSvgPath', 'links']),
...mapGetters(['currentProject']),
...mapGetters('pipelines', ['jobsCount', 'failedJobsCount', 'failedStages', 'pipelineFailed']),
...mapState('pipelines', ['isLoadingPipeline', 'latestPipeline', 'stages', 'isLoadingJobs']),
ciLintText() {
return sprintf(
__('You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}'),
{
linkStart: `<a href="${_.escape(this.currentProject.web_url)}/-/ci/lint">`,
linkEnd: '</a>',
},
false,
);
},
showLoadingIcon() {
return this.isLoadingPipeline && this.latestPipeline === null;
},
},
created() {
this.fetchLatestPipeline();
},
methods: {
...mapActions('pipelines', ['fetchLatestPipeline']),
},
};
</script>
<template>
<div class="ide-pipeline">
<loading-icon
v-if="showLoadingIcon"
class="prepend-top-default"
size="2"
/>
<template v-else-if="latestPipeline !== null">
<header
v-if="latestPipeline"
class="ide-tree-header ide-pipeline-header"
>
<ci-icon
:status="latestPipeline.details.status"
:size="24"
/>
<span class="prepend-left-8">
<strong>
{{ __('Pipeline') }}
</strong>
<a
:href="latestPipeline.path"
target="_blank"
class="ide-external-link"
>
#{{ latestPipeline.id }}
<icon
name="external-link"
:size="12"
/>
</a>
</span>
</header>
<empty-state
v-if="latestPipeline === false"
:help-page-path="links.ciHelpPagePath"
:empty-state-svg-path="pipelinesEmptyStateSvgPath"
:can-set-ci="true"
/>
<div
v-else-if="latestPipeline.yamlError"
class="bs-callout bs-callout-danger"
>
<p class="append-bottom-0">
{{ __('Found errors in your .gitlab-ci.yml:') }}
</p>
<p class="append-bottom-0">
{{ latestPipeline.yamlError }}
</p>
<p
class="append-bottom-0"
v-html="ciLintText"
></p>
</div>
<tabs
v-else
class="ide-pipeline-list"
>
<tab
:active="!pipelineFailed"
>
<template slot="title">
{{ __('Jobs') }}
<span
v-if="jobsCount"
class="badge badge-pill"
>
{{ jobsCount }}
</span>
</template>
<jobs-list
:loading="isLoadingJobs"
:stages="stages"
/>
</tab>
<tab
:active="pipelineFailed"
>
<template slot="title">
{{ __('Failed Jobs') }}
<span
v-if="failedJobsCount"
class="badge badge-pill"
>
{{ failedJobsCount }}
</span>
</template>
<jobs-list
:loading="isLoadingJobs"
:stages="failedStages"
/>
</tab>
</tabs>
</template>
</div>
</template>
......@@ -6,12 +6,12 @@ import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer
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) {
......
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');
......
/* 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) => {
......
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}`,
......
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 = ({ rootGetters, commit }, { pipelines }) => {
let lastCommitPipeline = false;
if (pipelines && pipelines.length) {
const lastCommitHash = rootGetters.lastCommit && rootGetters.lastCommit.id;
lastCommitPipeline = pipelines.find(pipeline => pipeline.commit.id === lastCommitHash);
}
commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, lastCommitPipeline);
};
export const receiveLatestPipelineSuccess = ({ commit }, pipeline) =>
commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, pipeline);
export const fetchLatestPipeline = ({ dispatch, rootState }, sha) => {
export const fetchLatestPipeline = ({ dispatch, rootGetters }) => {
if (eTagPoll) return;
dispatch('requestLatestPipeline');
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 () => {};
// eslint-disable-next-line import/prefer-default-export
export const states = {
failed: 'failed',
};
import { states } from './constants';
export const hasLatestPipeline = state => !state.isLoadingPipeline && !!state.latestPipeline;
export const 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';
/* 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,
}));
},
};
export default () => ({
isLoadingPipeline: false,
isLoadingPipeline: true,
isLoadingJobs: false,
latestPipeline: null,
stages: [],
......
// eslint-disable-next-line import/prefer-default-export
export const normalizeJob = job => ({
id: job.id,
name: job.name,
status: job.status,
path: job.build_path,
});
......@@ -6,6 +6,7 @@ export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
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);
......
/* 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());
}
......
/* 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>
<script>
export default {
props: {
title: {
type: String,
required: false,
default: '',
},
active: {
type: Boolean,
required: false,
default: false,
},
},
data() {
return {
// props can't be updated, so we map it to data where we can
localActive: this.active,
};
},
watch: {
active() {
this.localActive = this.active;
},
},
created() {
this.isTab = true;
},
};
</script>
<template>
<div
class="tab-pane"
:class="{
active: localActive
}"
role="tabpanel"
>
<slot></slot>
</div>
</template>
export default {
data() {
return {
currentIndex: 0,
tabs: [],
};
},
mounted() {
this.updateTabs();
},
methods: {
updateTabs() {
this.tabs = this.$children.filter(child => child.isTab);
this.currentIndex = this.tabs.findIndex(tab => tab.localActive);
},
setTab(index) {
this.tabs[this.currentIndex].localActive = false;
this.tabs[index].localActive = true;
this.currentIndex = index;
},
},
render(h) {
const navItems = this.tabs.map((tab, i) =>
h(
'li',
{
key: i,
},
[
h(
'a',
{
class: tab.localActive ? 'active' : null,
attrs: {
href: '#',
},
on: {
click: () => this.setTab(i),
},
},
tab.$slots.title || tab.title,
),
],
),
);
const nav = h(
'ul',
{
class: 'nav-links tab-links',
},
[navItems],
);
const content = h(
'div',
{
class: ['tab-content'],
},
[this.$slots.default],
);
return h('div', {}, [[nav], content]);
},
};
......@@ -2,7 +2,9 @@ import $ from 'jquery';
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 {
......
/*
* 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 {
color: $black;
font-weight: $gl-font-weight-bold;
&.active a,
a.active {
color: $black;
font-weight: $gl-font-weight-bold;
.badge.badge-pill {
color: $black;
}
.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
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
......
module CalendarHelper
def calendar_url_options
{ format: :ics,
feed_token: current_user.try(:feed_token),
due_date: Issue::DueNextMonthAndPreviousTwoWeeks.name,
sort: 'closest_future_date' }
end
end
module RssHelper
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
......
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?
......
# 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.
......
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册