Merge branch 'master' into fix/gb/pipeline-retry-builds-started
* master: (313 commits) Allow slashes in slash command arguments Add API endpoint to get all milestone merge requests remove trailing comma Restore pagination to admin abuse reports replace deprecated NoErrorsPlugin with NoEmitOnErrorsPlugin only compress assets in production Reduce number of pipelines created to test pagination add CHANGELOG.md entry for !8761 prevent diff unfolding link from appearing for deleted files fix build failures only show diff unfolding link if there are more lines to show fix typo in node section Only yield valid references in ReferenceFilter.references_in Cache js selectors; fix css move "Install node modules" step before "Migrate DB" within update process Renders pagination again for pipelines table update migration docs for 8.17 to include minimum node version Add CHANGELOG file Fix positioning of top scroll button Remove comments in migration ...
This commit is contained in:
commit
5f271a9fa2
638 changed files with 13916 additions and 6910 deletions
14
.eslintrc
14
.eslintrc
|
@ -12,12 +12,18 @@
|
|||
"localStorage": false
|
||||
},
|
||||
"plugins": [
|
||||
"filenames"
|
||||
"filenames",
|
||||
"import"
|
||||
],
|
||||
"settings": {
|
||||
"import/resolver": {
|
||||
"webpack": {
|
||||
"config": "./config/webpack.config.js"
|
||||
}
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"],
|
||||
"no-multiple-empty-lines": ["error", { "max": 1 }],
|
||||
"import/no-extraneous-dependencies": "off",
|
||||
"import/no-unresolved": "off"
|
||||
"no-multiple-empty-lines": ["error", { "max": 1 }]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -107,7 +107,10 @@ setup-test-env:
|
|||
<<: *dedicated-runner
|
||||
stage: prepare
|
||||
script:
|
||||
- npm install
|
||||
- node --version
|
||||
- yarn --version
|
||||
- yarn install --pure-lockfile
|
||||
- yarn check # ensure that yarn.lock matches package.json
|
||||
- bundle exec rake gitlab:assets:compile
|
||||
- bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
|
||||
artifacts:
|
||||
|
@ -246,13 +249,12 @@ karma:
|
|||
<<: *use-db
|
||||
<<: *dedicated-runner
|
||||
script:
|
||||
- npm link istanbul
|
||||
- bundle exec rake karma
|
||||
artifacts:
|
||||
name: coverage-javascript
|
||||
expire_in: 31d
|
||||
paths:
|
||||
- coverage-javascript/default/
|
||||
- coverage-javascript/
|
||||
|
||||
lint-doc:
|
||||
stage: test
|
||||
|
@ -325,11 +327,9 @@ lint:javascript:
|
|||
paths:
|
||||
- node_modules/
|
||||
stage: test
|
||||
image: "node:7.1"
|
||||
before_script:
|
||||
- npm install
|
||||
before_script: []
|
||||
script:
|
||||
- npm --silent run eslint
|
||||
- yarn run eslint
|
||||
|
||||
lint:javascript:report:
|
||||
<<: *dedicated-runner
|
||||
|
@ -337,12 +337,10 @@ lint:javascript:report:
|
|||
paths:
|
||||
- node_modules/
|
||||
stage: post-test
|
||||
image: "node:7.1"
|
||||
before_script:
|
||||
- npm install
|
||||
before_script: []
|
||||
script:
|
||||
- find app/ spec/ -name '*.js' -or -name '*.js.es6' -exec sed --in-place 's|/\* eslint-disable .*\*/||' {} \; # run report over all files
|
||||
- npm --silent run eslint-report || true # ignore exit code
|
||||
- yarn run eslint-report || true # ignore exit code
|
||||
artifacts:
|
||||
name: eslint-report
|
||||
expire_in: 31d
|
||||
|
@ -395,7 +393,7 @@ pages:
|
|||
- mv public/ .public/
|
||||
- mkdir public/
|
||||
- mv coverage/ public/coverage-ruby/ || true
|
||||
- mv coverage-javascript/default/ public/coverage-javascript/ || true
|
||||
- mv coverage-javascript/ public/coverage-javascript/ || true
|
||||
- mv eslint-report.html public/ || true
|
||||
artifacts:
|
||||
paths:
|
||||
|
|
18
.rubocop.yml
18
.rubocop.yml
|
@ -5,7 +5,7 @@ require:
|
|||
inherit_from: .rubocop_todo.yml
|
||||
|
||||
AllCops:
|
||||
TargetRubyVersion: 2.1
|
||||
TargetRubyVersion: 2.3
|
||||
# Cop names are not d§splayed in offense messages by default. Change behavior
|
||||
# by overriding DisplayCopNames, or by giving the -D/--display-cop-names
|
||||
# option.
|
||||
|
@ -339,6 +339,10 @@ Style/OpMethod:
|
|||
Style/ParenthesesAroundCondition:
|
||||
Enabled: true
|
||||
|
||||
# Checks for an obsolete RuntimeException argument in raise/fail.
|
||||
Style/RedundantException:
|
||||
Enabled: true
|
||||
|
||||
# Checks for parentheses that seem not to serve any purpose.
|
||||
Style/RedundantParentheses:
|
||||
Enabled: true
|
||||
|
@ -568,6 +572,10 @@ Lint/ElseLayout:
|
|||
Lint/EmptyEnsure:
|
||||
Enabled: true
|
||||
|
||||
# Checks for the presence of `when` branches without a body.
|
||||
Lint/EmptyWhen:
|
||||
Enabled: true
|
||||
|
||||
# Align ends correctly.
|
||||
Lint/EndAlignment:
|
||||
Enabled: true
|
||||
|
@ -769,6 +777,10 @@ Rails/ScopeArgs:
|
|||
RSpec/AnyInstance:
|
||||
Enabled: false
|
||||
|
||||
# Check for expectations where `be(...)` can replace `eql(...)`.
|
||||
RSpec/BeEql:
|
||||
Enabled: false
|
||||
|
||||
# Check that the first argument to the top level describe is the tested class or
|
||||
# module.
|
||||
RSpec/DescribeClass:
|
||||
|
@ -797,6 +809,10 @@ RSpec/ExampleWording:
|
|||
not: does not
|
||||
IgnoredWords: []
|
||||
|
||||
# Checks for `expect(...)` calls containing literal values.
|
||||
RSpec/ExpectActual:
|
||||
Enabled: true
|
||||
|
||||
# Checks the file and folder naming of the spec file.
|
||||
RSpec/FilePath:
|
||||
Enabled: false
|
||||
|
|
|
@ -21,10 +21,6 @@ Lint/AmbiguousRegexpLiteral:
|
|||
Lint/AssignmentInCondition:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 1
|
||||
Lint/EmptyWhen:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 20
|
||||
Lint/HandleExceptions:
|
||||
Enabled: false
|
||||
|
@ -80,19 +76,11 @@ Performance/RedundantMatch:
|
|||
Performance/RedundantMerge:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 7
|
||||
RSpec/BeEql:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 15
|
||||
# Configuration parameters: CustomIncludeMethods.
|
||||
RSpec/EmptyExampleGroup:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 24
|
||||
RSpec/ExpectActual:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 58
|
||||
# Configuration parameters: EnforcedStyle, SupportedStyles.
|
||||
# SupportedStyles: implicit, each, example
|
||||
|
@ -424,11 +412,6 @@ Style/RaiseArgs:
|
|||
Style/RedundantBegin:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 1
|
||||
# Cop supports --auto-correct.
|
||||
Style/RedundantException:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 29
|
||||
# Cop supports --auto-correct.
|
||||
Style/RedundantFreeze:
|
||||
|
|
21
CHANGELOG.md
21
CHANGELOG.md
|
@ -2,6 +2,13 @@
|
|||
documentation](doc/development/changelog.md) for instructions on adding your own
|
||||
entry.
|
||||
|
||||
## 8.16.5 (2017-02-14)
|
||||
|
||||
- Patch Asciidocs rendering to block XSS.
|
||||
- Fix XSS vulnerability in SVG attachments.
|
||||
- Prevent the GitHub importer from assigning labels and comments to merge requests or issues belonging to other projects.
|
||||
- Patch XSS vulnerability in RDOC support.
|
||||
|
||||
## 8.16.4 (2017-02-02)
|
||||
|
||||
- Support non-ASCII characters in GFM autocomplete. !8729
|
||||
|
@ -174,6 +181,13 @@ entry.
|
|||
- Add margin to markdown math blocks.
|
||||
- Add hover state to MR comment reply button.
|
||||
|
||||
## 8.15.6 (2017-02-14)
|
||||
|
||||
- Patch Asciidocs rendering to block XSS.
|
||||
- Fix XSS vulnerability in SVG attachments.
|
||||
- Prevent the GitHub importer from assigning labels and comments to merge requests or issues belonging to other projects.
|
||||
- Patch XSS vulnerability in RDOC support.
|
||||
|
||||
## 8.15.4 (2017-01-09)
|
||||
|
||||
- Make successful pipeline emails off for watchers. !8176
|
||||
|
@ -437,6 +451,13 @@ entry.
|
|||
- Whitelist next project names: help, ci, admin, search. !8227
|
||||
- Adds back CSS for progress-bars. !8237
|
||||
|
||||
## 8.14.9 (2017-02-14)
|
||||
|
||||
- Patch Asciidocs rendering to block XSS.
|
||||
- Fix XSS vulnerability in SVG attachments.
|
||||
- Prevent the GitHub importer from assigning labels and comments to merge requests or issues belonging to other projects.
|
||||
- Patch XSS vulnerability in RDOC support.
|
||||
|
||||
## 8.14.8 (2017-01-25)
|
||||
|
||||
- Accept environment variables from the `pre-receive` script. !7967
|
||||
|
|
1
Gemfile
1
Gemfile
|
@ -29,6 +29,7 @@ gem 'omniauth-github', '~> 1.1.1'
|
|||
gem 'omniauth-gitlab', '~> 1.0.2'
|
||||
gem 'omniauth-google-oauth2', '~> 0.4.1'
|
||||
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
|
||||
gem 'omniauth-oauth2-generic', '~> 0.2.2'
|
||||
gem 'omniauth-saml', '~> 1.7.0'
|
||||
gem 'omniauth-shibboleth', '~> 1.2.0'
|
||||
gem 'omniauth-twitter', '~> 1.2.0'
|
||||
|
|
|
@ -483,6 +483,8 @@ GEM
|
|||
omniauth-oauth2 (1.3.1)
|
||||
oauth2 (~> 1.0)
|
||||
omniauth (~> 1.2)
|
||||
omniauth-oauth2-generic (0.2.2)
|
||||
omniauth-oauth2 (~> 1.0)
|
||||
omniauth-saml (1.7.0)
|
||||
omniauth (~> 1.3)
|
||||
ruby-saml (~> 1.4)
|
||||
|
@ -931,6 +933,7 @@ DEPENDENCIES
|
|||
omniauth-gitlab (~> 1.0.2)
|
||||
omniauth-google-oauth2 (~> 0.4.1)
|
||||
omniauth-kerberos (~> 0.3.0)
|
||||
omniauth-oauth2-generic (~> 0.2.2)
|
||||
omniauth-saml (~> 1.7.0)
|
||||
omniauth-shibboleth (~> 1.2.0)
|
||||
omniauth-twitter (~> 1.2.0)
|
||||
|
|
BIN
app/assets/images/favicon-blue.ico
Normal file
BIN
app/assets/images/favicon-blue.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.3 KiB |
|
@ -11,7 +11,7 @@
|
|||
licensePath: "/api/:version/templates/licenses/:key",
|
||||
gitignorePath: "/api/:version/templates/gitignores/:key",
|
||||
gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key",
|
||||
dockerfilePath: "/api/:version/dockerfiles/:key",
|
||||
dockerfilePath: "/api/:version/templates/dockerfiles/:key",
|
||||
issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key",
|
||||
group: function(group_id, callback) {
|
||||
var url = Api.buildUrl(Api.groupPath)
|
||||
|
|
|
@ -56,8 +56,7 @@ requireAll(require.context('./u2f', false, /^\.\/.*\.(js|es6)$/));
|
|||
requireAll(require.context('./droplab', false, /^\.\/.*\.(js|es6)$/));
|
||||
requireAll(require.context('.', false, /^\.\/(?!application\.js).*\.(js|es6)$/));
|
||||
require('vendor/fuzzaldrin-plus');
|
||||
window.ES6Promise = require('vendor/es6-promise.auto');
|
||||
window.ES6Promise.polyfill();
|
||||
require('es6-promise').polyfill();
|
||||
|
||||
(function () {
|
||||
document.addEventListener('beforeunload', function () {
|
||||
|
@ -102,11 +101,6 @@ window.ES6Promise.polyfill();
|
|||
}
|
||||
});
|
||||
|
||||
$('.nav-sidebar').niceScroll({
|
||||
cursoropacitymax: '0.4',
|
||||
cursorcolor: '#FFF',
|
||||
cursorborder: '1px solid #FFF'
|
||||
});
|
||||
$('.js-select-on-focus').on('focusin', function () {
|
||||
return $(this).select().one('mouseup', function (e) {
|
||||
return e.preventDefault();
|
||||
|
@ -246,8 +240,6 @@ window.ES6Promise.polyfill();
|
|||
});
|
||||
gl.awardsHandler = new AwardsHandler();
|
||||
new Aside();
|
||||
// bind sidebar events
|
||||
new gl.Sidebar();
|
||||
|
||||
gl.utils.initTimeagoTimeout();
|
||||
});
|
||||
|
|
|
@ -95,7 +95,7 @@ $(() => {
|
|||
},
|
||||
computed: {
|
||||
disabled() {
|
||||
return Store.shouldAddBlankState();
|
||||
return !this.store.lists.filter(list => list.type !== 'blank' && list.type !== 'done').length;
|
||||
},
|
||||
},
|
||||
template: `
|
||||
|
|
|
@ -3,5 +3,5 @@
|
|||
|
||||
Vue.filter('due-date', (value) => {
|
||||
const date = new Date(value);
|
||||
return dateFormat(date, 'mmm d, yyyy');
|
||||
return dateFormat(date, 'mmm d, yyyy', true);
|
||||
});
|
||||
|
|
|
@ -20,7 +20,10 @@ $(() => {
|
|||
gl.commits.PipelinesTableBundle.$destroy(true);
|
||||
}
|
||||
|
||||
gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView({
|
||||
el: document.querySelector('#commit-pipeline-table-view'),
|
||||
});
|
||||
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
|
||||
gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView();
|
||||
|
||||
if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) {
|
||||
gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -8,7 +8,22 @@
|
|||
* Uses Vue.Resource
|
||||
*/
|
||||
class PipelinesService {
|
||||
constructor(endpoint) {
|
||||
|
||||
/**
|
||||
* FIXME: The url provided to request the pipelines in the new merge request
|
||||
* page already has `.json`.
|
||||
* This should be fixed when the endpoint is improved.
|
||||
*
|
||||
* @param {String} root
|
||||
*/
|
||||
constructor(root) {
|
||||
let endpoint;
|
||||
|
||||
if (root.indexOf('.json') === -1) {
|
||||
endpoint = `${root}.json`;
|
||||
} else {
|
||||
endpoint = root;
|
||||
}
|
||||
this.pipelines = Vue.resource(endpoint);
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
*
|
||||
* Used to store the Pipelines rendered in the commit view in the pipelines table.
|
||||
*/
|
||||
require('../../vue_realtime_listener');
|
||||
|
||||
class PipelinesStore {
|
||||
constructor() {
|
||||
|
@ -24,7 +25,7 @@ class PipelinesStore {
|
|||
* update the time to show how long as passed.
|
||||
*
|
||||
*/
|
||||
startTimeAgoLoops() {
|
||||
static startTimeAgoLoops() {
|
||||
const startTimeLoops = () => {
|
||||
this.timeLoopInterval = setInterval(() => {
|
||||
this.$children[0].$children.reduce((acc, component) => {
|
||||
|
@ -44,7 +45,4 @@ class PipelinesStore {
|
|||
}
|
||||
}
|
||||
|
||||
window.gl = window.gl || {};
|
||||
gl.commits = gl.commits || {};
|
||||
gl.commits.pipelines = gl.commits.pipelines || {};
|
||||
gl.commits.pipelines.PipelinesStore = PipelinesStore;
|
||||
module.exports = PipelinesStore;
|
||||
|
|
|
@ -6,9 +6,8 @@ window.Vue.use(require('vue-resource'));
|
|||
require('../../lib/utils/common_utils');
|
||||
require('../../vue_shared/vue_resource_interceptor');
|
||||
require('../../vue_shared/components/pipelines_table');
|
||||
require('../../vue_realtime_listener/index');
|
||||
require('./pipelines_service');
|
||||
require('./pipelines_store');
|
||||
const PipelineStore = require('./pipelines_store');
|
||||
|
||||
/**
|
||||
*
|
||||
|
@ -41,7 +40,7 @@ require('./pipelines_store');
|
|||
data() {
|
||||
const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset;
|
||||
const svgsData = document.querySelector('.pipeline-svgs').dataset;
|
||||
const store = new gl.commits.pipelines.PipelinesStore();
|
||||
const store = new PipelineStore();
|
||||
|
||||
// Transform svgs DOMStringMap to a plain Object.
|
||||
const svgsObject = gl.utils.DOMStringMapToObject(svgsData);
|
||||
|
@ -56,15 +55,14 @@ require('./pipelines_store');
|
|||
},
|
||||
|
||||
/**
|
||||
* When the component is created the service to fetch the data will be
|
||||
* initialized with the correct endpoint.
|
||||
* When the component is about to be mounted, tell the service to fetch the data
|
||||
*
|
||||
* A request to fetch the pipelines will be made.
|
||||
* In case of a successfull response we will store the data in the provided
|
||||
* store, in case of a failed response we need to warn the user.
|
||||
*
|
||||
*/
|
||||
created() {
|
||||
beforeMount() {
|
||||
const pipelinesService = new gl.commits.pipelines.PipelinesService(this.endpoint);
|
||||
|
||||
this.isLoading = true;
|
||||
|
@ -72,7 +70,6 @@ require('./pipelines_store');
|
|||
.then(response => response.json())
|
||||
.then((json) => {
|
||||
this.store.storePipelines(json);
|
||||
this.store.startTimeAgoLoops.call(this, Vue);
|
||||
this.isLoading = false;
|
||||
})
|
||||
.catch(() => {
|
||||
|
@ -81,9 +78,15 @@ require('./pipelines_store');
|
|||
});
|
||||
},
|
||||
|
||||
beforeUpdate() {
|
||||
if (this.state.pipelines.length && this.$children) {
|
||||
PipelineStore.startTimeAgoLoops.call(this, Vue);
|
||||
}
|
||||
},
|
||||
|
||||
template: `
|
||||
<div>
|
||||
<div class="pipelines realtime-loading" v-if="isLoading">
|
||||
<div class="pipelines">
|
||||
<div class="realtime-loading" v-if="isLoading">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -13,6 +13,12 @@
|
|||
<div>
|
||||
<div class="events-description">
|
||||
{{ stage.description }}
|
||||
<span v-if="items.length === 50" class="events-info pull-right">
|
||||
<i class="fa fa-warning has-tooltip"
|
||||
title="Limited to showing 50 events at most"
|
||||
data-placement="top"></i>
|
||||
Showing 50 events
|
||||
</span>
|
||||
</div>
|
||||
<ul class="stage-event-list">
|
||||
<li v-for="commit in items" class="stage-event-item">
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
const calendar = new Pikaday({
|
||||
field: $dueDateInput.get(0),
|
||||
theme: 'gitlab-theme',
|
||||
format: 'YYYY-MM-DD',
|
||||
format: 'yyyy-mm-dd',
|
||||
onSelect: (dateText) => {
|
||||
const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd');
|
||||
|
||||
|
@ -63,6 +63,7 @@
|
|||
}
|
||||
});
|
||||
|
||||
calendar.setDate(new Date($dueDateInput.val()));
|
||||
this.$datePicker.append(calendar.el);
|
||||
this.$datePicker.data('pikaday', calendar);
|
||||
}
|
||||
|
@ -169,11 +170,12 @@
|
|||
const calendar = new Pikaday({
|
||||
field: $datePicker.get(0),
|
||||
theme: 'gitlab-theme',
|
||||
format: 'YYYY-MM-DD',
|
||||
format: 'yyyy-mm-dd',
|
||||
onSelect(dateText) {
|
||||
$datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
|
||||
}
|
||||
});
|
||||
calendar.setDate(new Date($datePicker.val()));
|
||||
|
||||
$datePicker.data('pikaday', calendar);
|
||||
});
|
||||
|
|
|
@ -1,223 +1,192 @@
|
|||
/* eslint-disable no-param-reassign, no-new */
|
||||
/* global Vue */
|
||||
/* global EnvironmentsService */
|
||||
/* global Flash */
|
||||
|
||||
window.Vue = require('vue');
|
||||
window.Vue.use(require('vue-resource'));
|
||||
require('../services/environments_service');
|
||||
require('./environment_item');
|
||||
const Vue = require('vue');
|
||||
Vue.use(require('vue-resource'));
|
||||
const EnvironmentsService = require('../services/environments_service');
|
||||
const EnvironmentTable = require('./environments_table');
|
||||
const EnvironmentsStore = require('../stores/environments_store');
|
||||
require('../../vue_shared/components/table_pagination');
|
||||
require('../../lib/utils/common_utils');
|
||||
|
||||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
module.exports = Vue.component('environment-component', {
|
||||
|
||||
gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', {
|
||||
props: {
|
||||
store: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({}),
|
||||
},
|
||||
components: {
|
||||
'environment-table': EnvironmentTable,
|
||||
'table-pagination': gl.VueGlPagination,
|
||||
},
|
||||
|
||||
data() {
|
||||
const environmentsData = document.querySelector('#environments-list-view').dataset;
|
||||
const store = new EnvironmentsStore();
|
||||
|
||||
return {
|
||||
store,
|
||||
state: store.state,
|
||||
visibility: 'available',
|
||||
isLoading: false,
|
||||
cssContainerClass: environmentsData.cssClass,
|
||||
endpoint: environmentsData.environmentsDataEndpoint,
|
||||
canCreateDeployment: environmentsData.canCreateDeployment,
|
||||
canReadEnvironment: environmentsData.canReadEnvironment,
|
||||
canCreateEnvironment: environmentsData.canCreateEnvironment,
|
||||
projectEnvironmentsPath: environmentsData.projectEnvironmentsPath,
|
||||
projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath,
|
||||
newEnvironmentPath: environmentsData.newEnvironmentPath,
|
||||
helpPagePath: environmentsData.helpPagePath,
|
||||
commitIconSvg: environmentsData.commitIconSvg,
|
||||
playIconSvg: environmentsData.playIconSvg,
|
||||
terminalIconSvg: environmentsData.terminalIconSvg,
|
||||
|
||||
// Pagination Properties,
|
||||
paginationInformation: {},
|
||||
pageNumber: 1,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
scope() {
|
||||
return gl.utils.getParameterByName('scope');
|
||||
},
|
||||
|
||||
components: {
|
||||
'environment-item': gl.environmentsList.EnvironmentItem,
|
||||
canReadEnvironmentParsed() {
|
||||
return gl.utils.convertPermissionToBoolean(this.canReadEnvironment);
|
||||
},
|
||||
|
||||
data() {
|
||||
const environmentsData = document.querySelector('#environments-list-view').dataset;
|
||||
|
||||
return {
|
||||
state: this.store.state,
|
||||
visibility: 'available',
|
||||
isLoading: false,
|
||||
cssContainerClass: environmentsData.cssClass,
|
||||
endpoint: environmentsData.environmentsDataEndpoint,
|
||||
canCreateDeployment: environmentsData.canCreateDeployment,
|
||||
canReadEnvironment: environmentsData.canReadEnvironment,
|
||||
canCreateEnvironment: environmentsData.canCreateEnvironment,
|
||||
projectEnvironmentsPath: environmentsData.projectEnvironmentsPath,
|
||||
projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath,
|
||||
newEnvironmentPath: environmentsData.newEnvironmentPath,
|
||||
helpPagePath: environmentsData.helpPagePath,
|
||||
commitIconSvg: environmentsData.commitIconSvg,
|
||||
playIconSvg: environmentsData.playIconSvg,
|
||||
terminalIconSvg: environmentsData.terminalIconSvg,
|
||||
};
|
||||
canCreateDeploymentParsed() {
|
||||
return gl.utils.convertPermissionToBoolean(this.canCreateDeployment);
|
||||
},
|
||||
|
||||
computed: {
|
||||
scope() {
|
||||
return this.$options.getQueryParameter('scope');
|
||||
},
|
||||
canCreateEnvironmentParsed() {
|
||||
return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment);
|
||||
},
|
||||
|
||||
canReadEnvironmentParsed() {
|
||||
return this.$options.convertPermissionToBoolean(this.canReadEnvironment);
|
||||
},
|
||||
},
|
||||
|
||||
canCreateDeploymentParsed() {
|
||||
return this.$options.convertPermissionToBoolean(this.canCreateDeployment);
|
||||
},
|
||||
/**
|
||||
* Fetches all the environments and stores them.
|
||||
* Toggles loading property.
|
||||
*/
|
||||
created() {
|
||||
const scope = gl.utils.getParameterByName('scope') || this.visibility;
|
||||
const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
|
||||
|
||||
canCreateEnvironmentParsed() {
|
||||
return this.$options.convertPermissionToBoolean(this.canCreateEnvironment);
|
||||
},
|
||||
const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
|
||||
|
||||
const service = new EnvironmentsService(endpoint);
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
return service.all()
|
||||
.then(resp => ({
|
||||
headers: resp.headers,
|
||||
body: resp.json(),
|
||||
}))
|
||||
.then((response) => {
|
||||
this.store.storeAvailableCount(response.body.available_count);
|
||||
this.store.storeStoppedCount(response.body.stopped_count);
|
||||
this.store.storeEnvironments(response.body.environments);
|
||||
this.store.setPagination(response.headers);
|
||||
})
|
||||
.then(() => {
|
||||
this.isLoading = false;
|
||||
})
|
||||
.catch(() => {
|
||||
this.isLoading = false;
|
||||
new Flash('An error occurred while fetching the environments.', 'alert');
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleRow(model) {
|
||||
return this.store.toggleFolder(model.name);
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetches all the environments and stores them.
|
||||
* Toggles loading property.
|
||||
*/
|
||||
created() {
|
||||
gl.environmentsService = new EnvironmentsService(this.endpoint);
|
||||
|
||||
const scope = this.$options.getQueryParameter('scope');
|
||||
if (scope) {
|
||||
this.store.storeVisibility(scope);
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
return gl.environmentsService.all()
|
||||
.then(resp => resp.json())
|
||||
.then((json) => {
|
||||
this.store.storeEnvironments(json);
|
||||
this.isLoading = false;
|
||||
})
|
||||
.catch(() => {
|
||||
this.isLoading = false;
|
||||
new Flash('An error occurred while fetching the environments.', 'alert');
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Transforms the url parameter into an object and
|
||||
* returns the one requested.
|
||||
* Will change the page number and update the URL.
|
||||
*
|
||||
* @param {String} param
|
||||
* @returns {String} The value of the requested parameter.
|
||||
* @param {Number} pageNumber desired page to go to.
|
||||
* @return {String}
|
||||
*/
|
||||
getQueryParameter(parameter) {
|
||||
return window.location.search.substring(1).split('&').reduce((acc, param) => {
|
||||
const paramSplited = param.split('=');
|
||||
acc[paramSplited[0]] = paramSplited[1];
|
||||
return acc;
|
||||
}, {})[parameter];
|
||||
},
|
||||
changePage(pageNumber) {
|
||||
const param = gl.utils.setParamInURL('page', pageNumber);
|
||||
|
||||
/**
|
||||
* Converts permission provided as strings to booleans.
|
||||
* @param {String} string
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
convertPermissionToBoolean(string) {
|
||||
return string === 'true';
|
||||
gl.utils.visitUrl(param);
|
||||
return param;
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleRow(model) {
|
||||
return this.store.toggleFolder(model.name);
|
||||
},
|
||||
},
|
||||
|
||||
template: `
|
||||
<div :class="cssContainerClass">
|
||||
<div class="top-area">
|
||||
<ul v-if="!isLoading" class="nav-links">
|
||||
<li v-bind:class="{ 'active': scope === undefined }">
|
||||
<a :href="projectEnvironmentsPath">
|
||||
Available
|
||||
<span class="badge js-available-environments-count">
|
||||
{{state.availableCounter}}
|
||||
</span>
|
||||
</a>
|
||||
</li><li v-bind:class="{ 'active' : scope === 'stopped' }">
|
||||
<a :href="projectStoppedEnvironmentsPath">
|
||||
Stopped
|
||||
<span class="badge js-stopped-environments-count">
|
||||
{{state.stoppedCounter}}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="canCreateEnvironmentParsed && !isLoading" class="nav-controls">
|
||||
<a :href="newEnvironmentPath" class="btn btn-create">
|
||||
New environment
|
||||
template: `
|
||||
<div :class="cssContainerClass">
|
||||
<div class="top-area">
|
||||
<ul v-if="!isLoading" class="nav-links">
|
||||
<li v-bind:class="{ 'active': scope === null || scope === 'available' }">
|
||||
<a :href="projectEnvironmentsPath">
|
||||
Available
|
||||
<span class="badge js-available-environments-count">
|
||||
{{state.availableCounter}}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="environments-container">
|
||||
<div class="environments-list-loading text-center" v-if="isLoading">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
|
||||
<div class="blank-state blank-state-no-icon"
|
||||
v-if="!isLoading && state.environments.length === 0">
|
||||
<h2 class="blank-state-title js-blank-state-title">
|
||||
You don't have any environments right now.
|
||||
</h2>
|
||||
<p class="blank-state-text">
|
||||
Environments are places where code gets deployed, such as staging or production.
|
||||
<br />
|
||||
<a :href="helpPagePath">
|
||||
Read more about environments
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<a
|
||||
v-if="canCreateEnvironmentParsed"
|
||||
:href="newEnvironmentPath"
|
||||
class="btn btn-create js-new-environment-button">
|
||||
New Environment
|
||||
</li>
|
||||
<li v-bind:class="{ 'active' : scope === 'stopped' }">
|
||||
<a :href="projectStoppedEnvironmentsPath">
|
||||
Stopped
|
||||
<span class="badge js-stopped-environments-count">
|
||||
{{state.stoppedCounter}}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="table-holder"
|
||||
v-if="!isLoading && state.filteredEnvironments.length > 0">
|
||||
<table class="table ci-table environments">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="environments-name">Environment</th>
|
||||
<th class="environments-deploy">Last deployment</th>
|
||||
<th class="environments-build">Job</th>
|
||||
<th class="environments-commit">Commit</th>
|
||||
<th class="environments-date">Updated</th>
|
||||
<th class="hidden-xs environments-actions"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="model in state.filteredEnvironments"
|
||||
v-bind:model="model">
|
||||
|
||||
<tr
|
||||
is="environment-item"
|
||||
:model="model"
|
||||
:toggleRow="toggleRow.bind(model)"
|
||||
:can-create-deployment="canCreateDeploymentParsed"
|
||||
:can-read-environment="canReadEnvironmentParsed"
|
||||
:play-icon-svg="playIconSvg"
|
||||
:terminal-icon-svg="terminalIconSvg"
|
||||
:commit-icon-svg="commitIconSvg"></tr>
|
||||
|
||||
<tr v-if="model.isOpen && model.children && model.children.length > 0"
|
||||
is="environment-item"
|
||||
v-for="children in model.children"
|
||||
:model="children"
|
||||
:toggleRow="toggleRow.bind(children)"
|
||||
:can-create-deployment="canCreateDeploymentParsed"
|
||||
:can-read-environment="canReadEnvironmentParsed"
|
||||
:play-icon-svg="playIconSvg"
|
||||
:terminal-icon-svg="terminalIconSvg"
|
||||
:commit-icon-svg="commitIconSvg">
|
||||
</tr>
|
||||
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div v-if="canCreateEnvironmentParsed && !isLoading" class="nav-controls">
|
||||
<a :href="newEnvironmentPath" class="btn btn-create">
|
||||
New environment
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})();
|
||||
|
||||
<div class="environments-container">
|
||||
<div class="environments-list-loading text-center" v-if="isLoading">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
|
||||
<div class="blank-state blank-state-no-icon"
|
||||
v-if="!isLoading && state.environments.length === 0">
|
||||
<h2 class="blank-state-title js-blank-state-title">
|
||||
You don't have any environments right now.
|
||||
</h2>
|
||||
<p class="blank-state-text">
|
||||
Environments are places where code gets deployed, such as staging or production.
|
||||
<br />
|
||||
<a :href="helpPagePath">
|
||||
Read more about environments
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<a v-if="canCreateEnvironmentParsed"
|
||||
:href="newEnvironmentPath"
|
||||
class="btn btn-create js-new-environment-button">
|
||||
New Environment
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="table-holder"
|
||||
v-if="!isLoading && state.environments.length > 0">
|
||||
|
||||
<environment-table
|
||||
:environments="state.environments"
|
||||
:can-create-deployment="canCreateDeploymentParsed"
|
||||
:can-read-environment="canReadEnvironmentParsed"
|
||||
:play-icon-svg="playIconSvg"
|
||||
:terminal-icon-svg="terminalIconSvg"
|
||||
:commit-icon-svg="commitIconSvg">
|
||||
</environment-table>
|
||||
|
||||
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
|
||||
:change="changePage"
|
||||
:pageInfo="state.paginationInformation">
|
||||
</table-pagination>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
|
|
@ -1,50 +1,43 @@
|
|||
/* global Vue */
|
||||
const Vue = require('vue');
|
||||
|
||||
window.Vue = require('vue');
|
||||
|
||||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
window.gl.environmentsList = window.gl.environmentsList || {};
|
||||
|
||||
gl.environmentsList.ActionsComponent = Vue.component('actions-component', {
|
||||
props: {
|
||||
actions: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
playIconSvg: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
module.exports = Vue.component('actions-component', {
|
||||
props: {
|
||||
actions: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
|
||||
template: `
|
||||
<div class="inline">
|
||||
<div class="dropdown">
|
||||
<a class="dropdown-new btn btn-default" data-toggle="dropdown">
|
||||
<span class="js-dropdown-play-icon-container" v-html="playIconSvg"></span>
|
||||
<i class="fa fa-caret-down"></i>
|
||||
</a>
|
||||
playIconSvg: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
|
||||
<ul class="dropdown-menu dropdown-menu-align-right">
|
||||
<li v-for="action in actions">
|
||||
<a :href="action.play_path"
|
||||
data-method="post"
|
||||
rel="nofollow"
|
||||
class="js-manual-action-link">
|
||||
template: `
|
||||
<div class="inline">
|
||||
<div class="dropdown">
|
||||
<a class="dropdown-new btn btn-default" data-toggle="dropdown">
|
||||
<span class="js-dropdown-play-icon-container" v-html="playIconSvg"></span>
|
||||
<i class="fa fa-caret-down"></i>
|
||||
</a>
|
||||
|
||||
<span class="js-action-play-icon-container" v-html="playIconSvg"></span>
|
||||
<ul class="dropdown-menu dropdown-menu-align-right">
|
||||
<li v-for="action in actions">
|
||||
<a :href="action.play_path"
|
||||
data-method="post"
|
||||
rel="nofollow"
|
||||
class="js-manual-action-link">
|
||||
|
||||
<span>
|
||||
{{action.name}}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<span class="js-action-play-icon-container" v-html="playIconSvg"></span>
|
||||
|
||||
<span>
|
||||
{{action.name}}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})();
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
|
|
|
@ -1,23 +1,19 @@
|
|||
/* global Vue */
|
||||
/**
|
||||
* Renders the external url link in environments table.
|
||||
*/
|
||||
const Vue = require('vue');
|
||||
|
||||
window.Vue = require('vue');
|
||||
|
||||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
window.gl.environmentsList = window.gl.environmentsList || {};
|
||||
|
||||
gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', {
|
||||
props: {
|
||||
externalUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
module.exports = Vue.component('external-url-component', {
|
||||
props: {
|
||||
externalUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
template: `
|
||||
<a class="btn external_url" :href="externalUrl" target="_blank">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</a>
|
||||
`,
|
||||
});
|
||||
})();
|
||||
template: `
|
||||
<a class="btn external_url" :href="externalUrl" target="_blank">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</a>
|
||||
`,
|
||||
});
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,33 +1,30 @@
|
|||
/* global Vue */
|
||||
/**
|
||||
* Renders Rollback or Re deploy button in environments table depending
|
||||
* of the provided property `isLastDeployment`
|
||||
*/
|
||||
const Vue = require('vue');
|
||||
|
||||
window.Vue = require('vue');
|
||||
|
||||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
window.gl.environmentsList = window.gl.environmentsList || {};
|
||||
|
||||
gl.environmentsList.RollbackComponent = Vue.component('rollback-component', {
|
||||
props: {
|
||||
retryUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
isLastDeployment: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
module.exports = Vue.component('rollback-component', {
|
||||
props: {
|
||||
retryUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
|
||||
template: `
|
||||
<a class="btn" :href="retryUrl" data-method="post" rel="nofollow">
|
||||
<span v-if="isLastDeployment">
|
||||
Re-deploy
|
||||
</span>
|
||||
<span v-else>
|
||||
Rollback
|
||||
</span>
|
||||
</a>
|
||||
`,
|
||||
});
|
||||
})();
|
||||
isLastDeployment: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
template: `
|
||||
<a class="btn" :href="retryUrl" data-method="post" rel="nofollow">
|
||||
<span v-if="isLastDeployment">
|
||||
Re-deploy
|
||||
</span>
|
||||
<span v-else>
|
||||
Rollback
|
||||
</span>
|
||||
</a>
|
||||
`,
|
||||
});
|
||||
|
|
|
@ -1,27 +1,24 @@
|
|||
/* global Vue */
|
||||
/**
|
||||
* Renders the stop "button" that allows stop an environment.
|
||||
* Used in environments table.
|
||||
*/
|
||||
const Vue = require('vue');
|
||||
|
||||
window.Vue = require('vue');
|
||||
|
||||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
window.gl.environmentsList = window.gl.environmentsList || {};
|
||||
|
||||
gl.environmentsList.StopComponent = Vue.component('stop-component', {
|
||||
props: {
|
||||
stopUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
module.exports = Vue.component('stop-component', {
|
||||
props: {
|
||||
stopUrl: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
template: `
|
||||
<a class="btn stop-env-link"
|
||||
:href="stopUrl"
|
||||
data-confirm="Are you sure you want to stop this environment?"
|
||||
data-method="post"
|
||||
rel="nofollow">
|
||||
<i class="fa fa-stop stop-env-icon"></i>
|
||||
</a>
|
||||
`,
|
||||
});
|
||||
})();
|
||||
template: `
|
||||
<a class="btn stop-env-link"
|
||||
:href="stopUrl"
|
||||
data-confirm="Are you sure you want to stop this environment?"
|
||||
data-method="post"
|
||||
rel="nofollow">
|
||||
<i class="fa fa-stop stop-env-icon" aria-hidden="true"></i>
|
||||
</a>
|
||||
`,
|
||||
});
|
||||
|
|
|
@ -1,28 +1,25 @@
|
|||
/* global Vue */
|
||||
/**
|
||||
* Renders a terminal button to open a web terminal.
|
||||
* Used in environments table.
|
||||
*/
|
||||
const Vue = require('vue');
|
||||
|
||||
window.Vue = require('vue');
|
||||
|
||||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
window.gl.environmentsList = window.gl.environmentsList || {};
|
||||
|
||||
gl.environmentsList.TerminalButtonComponent = Vue.component('terminal-button-component', {
|
||||
props: {
|
||||
terminalPath: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
terminalIconSvg: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
module.exports = Vue.component('terminal-button-component', {
|
||||
props: {
|
||||
terminalPath: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
terminalIconSvg: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
template: `
|
||||
<a class="btn terminal-button"
|
||||
:href="terminalPath">
|
||||
<span class="js-terminal-icon-container" v-html="terminalIconSvg"></span>
|
||||
</a>
|
||||
`,
|
||||
});
|
||||
})();
|
||||
template: `
|
||||
<a class="btn terminal-button"
|
||||
:href="terminalPath">
|
||||
<span class="js-terminal-icon-container" v-html="terminalIconSvg"></span>
|
||||
</a>
|
||||
`,
|
||||
});
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
/**
|
||||
* Render environments table.
|
||||
*/
|
||||
const Vue = require('vue');
|
||||
const EnvironmentItem = require('./environment_item');
|
||||
|
||||
module.exports = Vue.component('environment-table-component', {
|
||||
|
||||
components: {
|
||||
'environment-item': EnvironmentItem,
|
||||
},
|
||||
|
||||
props: {
|
||||
environments: {
|
||||
type: Array,
|
||||
required: true,
|
||||
default: () => ([]),
|
||||
},
|
||||
|
||||
canReadEnvironment: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
|
||||
canCreateDeployment: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
|
||||
commitIconSvg: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
|
||||
playIconSvg: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
|
||||
terminalIconSvg: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
|
||||
template: `
|
||||
<table class="table ci-table environments">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="environments-name">Environment</th>
|
||||
<th class="environments-deploy">Last deployment</th>
|
||||
<th class="environments-build">Job</th>
|
||||
<th class="environments-commit">Commit</th>
|
||||
<th class="environments-date">Updated</th>
|
||||
<th class="hidden-xs environments-actions"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="model in environments"
|
||||
v-bind:model="model">
|
||||
<tr is="environment-item"
|
||||
:model="model"
|
||||
:can-create-deployment="canCreateDeployment"
|
||||
:can-read-environment="canReadEnvironment"
|
||||
:play-icon-svg="playIconSvg"
|
||||
:terminal-icon-svg="terminalIconSvg"
|
||||
:commit-icon-svg="commitIconSvg"></tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
`,
|
||||
});
|
|
@ -1,6 +1,4 @@
|
|||
window.Vue = require('vue');
|
||||
require('./stores/environments_store');
|
||||
require('./components/environment');
|
||||
const EnvironmentsComponent = require('./components/environment');
|
||||
require('../vue_shared/vue_resource_interceptor');
|
||||
|
||||
$(() => {
|
||||
|
@ -9,14 +7,8 @@ $(() => {
|
|||
if (gl.EnvironmentsListApp) {
|
||||
gl.EnvironmentsListApp.$destroy(true);
|
||||
}
|
||||
const Store = gl.environmentsList.EnvironmentsStore;
|
||||
|
||||
gl.EnvironmentsListApp = new gl.environmentsList.EnvironmentsComponent({
|
||||
gl.EnvironmentsListApp = new EnvironmentsComponent({
|
||||
el: document.querySelector('#environments-list-view'),
|
||||
|
||||
propsData: {
|
||||
store: Store.create(),
|
||||
},
|
||||
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
const EnvironmentsFolderComponent = require('./environments_folder_view');
|
||||
require('../../vue_shared/vue_resource_interceptor');
|
||||
|
||||
$(() => {
|
||||
window.gl = window.gl || {};
|
||||
|
||||
if (gl.EnvironmentsListFolderApp) {
|
||||
gl.EnvironmentsListFolderApp.$destroy(true);
|
||||
}
|
||||
|
||||
gl.EnvironmentsListFolderApp = new EnvironmentsFolderComponent({
|
||||
el: document.querySelector('#environments-folder-list-view'),
|
||||
});
|
||||
});
|
|
@ -0,0 +1,181 @@
|
|||
/* eslint-disable no-param-reassign, no-new */
|
||||
/* global Flash */
|
||||
|
||||
const Vue = require('vue');
|
||||
Vue.use(require('vue-resource'));
|
||||
const EnvironmentsService = require('../services/environments_service');
|
||||
const EnvironmentTable = require('../components/environments_table');
|
||||
const EnvironmentsStore = require('../stores/environments_store');
|
||||
require('../../vue_shared/components/table_pagination');
|
||||
require('../../lib/utils/common_utils');
|
||||
|
||||
module.exports = Vue.component('environment-folder-view', {
|
||||
|
||||
components: {
|
||||
'environment-table': EnvironmentTable,
|
||||
'table-pagination': gl.VueGlPagination,
|
||||
},
|
||||
|
||||
data() {
|
||||
const environmentsData = document.querySelector('#environments-folder-list-view').dataset;
|
||||
const store = new EnvironmentsStore();
|
||||
const pathname = window.location.pathname;
|
||||
const endpoint = `${pathname}.json`;
|
||||
const folderName = pathname.substr(pathname.lastIndexOf('/') + 1);
|
||||
|
||||
return {
|
||||
store,
|
||||
folderName,
|
||||
endpoint,
|
||||
state: store.state,
|
||||
visibility: 'available',
|
||||
isLoading: false,
|
||||
cssContainerClass: environmentsData.cssClass,
|
||||
canCreateDeployment: environmentsData.canCreateDeployment,
|
||||
canReadEnvironment: environmentsData.canReadEnvironment,
|
||||
|
||||
// svgs
|
||||
commitIconSvg: environmentsData.commitIconSvg,
|
||||
playIconSvg: environmentsData.playIconSvg,
|
||||
terminalIconSvg: environmentsData.terminalIconSvg,
|
||||
|
||||
// Pagination Properties,
|
||||
paginationInformation: {},
|
||||
pageNumber: 1,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
scope() {
|
||||
return gl.utils.getParameterByName('scope');
|
||||
},
|
||||
|
||||
canReadEnvironmentParsed() {
|
||||
return gl.utils.convertPermissionToBoolean(this.canReadEnvironment);
|
||||
},
|
||||
|
||||
canCreateDeploymentParsed() {
|
||||
return gl.utils.convertPermissionToBoolean(this.canCreateDeployment);
|
||||
},
|
||||
|
||||
/**
|
||||
* URL to link in the stopped tab.
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
stoppedPath() {
|
||||
return `${window.location.pathname}?scope=stopped`;
|
||||
},
|
||||
|
||||
/**
|
||||
* URL to link in the available tab.
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
availablePath() {
|
||||
return window.location.pathname;
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetches all the environments and stores them.
|
||||
* Toggles loading property.
|
||||
*/
|
||||
created() {
|
||||
const scope = gl.utils.getParameterByName('scope') || this.visibility;
|
||||
const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
|
||||
|
||||
const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
|
||||
|
||||
const service = new EnvironmentsService(endpoint);
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
return service.all()
|
||||
.then(resp => ({
|
||||
headers: resp.headers,
|
||||
body: resp.json(),
|
||||
}))
|
||||
.then((response) => {
|
||||
this.store.storeAvailableCount(response.body.available_count);
|
||||
this.store.storeStoppedCount(response.body.stopped_count);
|
||||
this.store.storeEnvironments(response.body.environments);
|
||||
this.store.setPagination(response.headers);
|
||||
})
|
||||
.then(() => {
|
||||
this.isLoading = false;
|
||||
})
|
||||
.catch(() => {
|
||||
this.isLoading = false;
|
||||
new Flash('An error occurred while fetching the environments.', 'alert');
|
||||
});
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Will change the page number and update the URL.
|
||||
*
|
||||
* @param {Number} pageNumber desired page to go to.
|
||||
*/
|
||||
changePage(pageNumber) {
|
||||
const param = gl.utils.setParamInURL('page', pageNumber);
|
||||
|
||||
gl.utils.visitUrl(param);
|
||||
return param;
|
||||
},
|
||||
},
|
||||
|
||||
template: `
|
||||
<div :class="cssContainerClass">
|
||||
<div class="top-area" v-if="!isLoading">
|
||||
|
||||
<h4 class="js-folder-name environments-folder-name">
|
||||
Environments / <b>{{folderName}}</b>
|
||||
</h4>
|
||||
|
||||
<ul class="nav-links">
|
||||
<li v-bind:class="{ 'active': scope === null || scope === 'available' }">
|
||||
<a :href="availablePath" class="js-available-environments-folder-tab">
|
||||
Available
|
||||
<span class="badge js-available-environments-count">
|
||||
{{state.availableCounter}}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
<li v-bind:class="{ 'active' : scope === 'stopped' }">
|
||||
<a :href="stoppedPath" class="js-stopped-environments-folder-tab">
|
||||
Stopped
|
||||
<span class="badge js-stopped-environments-count">
|
||||
{{state.stoppedCounter}}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="environments-container">
|
||||
<div class="environments-list-loading text-center" v-if="isLoading">
|
||||
<i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
|
||||
<div class="table-holder"
|
||||
v-if="!isLoading && state.environments.length > 0">
|
||||
|
||||
<environment-table
|
||||
:environments="state.environments"
|
||||
:can-create-deployment="canCreateDeploymentParsed"
|
||||
:can-read-environment="canReadEnvironmentParsed"
|
||||
:play-icon-svg="playIconSvg"
|
||||
:terminal-icon-svg="terminalIconSvg"
|
||||
:commit-icon-svg="commitIconSvg">
|
||||
</environment-table>
|
||||
|
||||
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
|
||||
:change="changePage"
|
||||
:pageInfo="state.paginationInformation">
|
||||
</table-pagination>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
});
|
|
@ -1,20 +1,8 @@
|
|||
/* globals Vue */
|
||||
/* eslint-disable no-unused-vars, no-param-reassign */
|
||||
const Vue = require('vue');
|
||||
|
||||
class EnvironmentsService {
|
||||
|
||||
constructor(root) {
|
||||
Vue.http.options.root = root;
|
||||
|
||||
this.environments = Vue.resource(root);
|
||||
|
||||
Vue.http.interceptors.push((request, next) => {
|
||||
// needed in order to not break the tests.
|
||||
if ($.rails) {
|
||||
request.headers['X-CSRF-Token'] = $.rails.csrfToken();
|
||||
}
|
||||
next();
|
||||
});
|
||||
constructor(endpoint) {
|
||||
this.environments = Vue.resource(endpoint);
|
||||
}
|
||||
|
||||
all() {
|
||||
|
@ -22,4 +10,4 @@ class EnvironmentsService {
|
|||
}
|
||||
}
|
||||
|
||||
window.EnvironmentsService = EnvironmentsService;
|
||||
module.exports = EnvironmentsService;
|
||||
|
|
|
@ -1,190 +1,90 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
window.gl.environmentsList = window.gl.environmentsList || {};
|
||||
require('~/lib/utils/common_utils');
|
||||
/**
|
||||
* Environments Store.
|
||||
*
|
||||
* Stores received environments, count of stopped environments and count of
|
||||
* available environments.
|
||||
*/
|
||||
class EnvironmentsStore {
|
||||
constructor() {
|
||||
this.state = {};
|
||||
this.state.environments = [];
|
||||
this.state.stoppedCounter = 0;
|
||||
this.state.availableCounter = 0;
|
||||
this.state.paginationInformation = {};
|
||||
|
||||
gl.environmentsList.EnvironmentsStore = {
|
||||
state: {},
|
||||
return this;
|
||||
}
|
||||
|
||||
create() {
|
||||
this.state.environments = [];
|
||||
this.state.stoppedCounter = 0;
|
||||
this.state.availableCounter = 0;
|
||||
this.state.visibility = 'available';
|
||||
this.state.filteredEnvironments = [];
|
||||
/**
|
||||
*
|
||||
* Stores the received environments.
|
||||
*
|
||||
* In the main environments endpoint, each environment has the following schema
|
||||
* { name: String, size: Number, latest: Object }
|
||||
* In the endpoint to retrieve environments from each folder, the environment does
|
||||
* not have the `latest` key and the data is all in the root level.
|
||||
* To avoid doing this check in the view, we store both cases the same by extracting
|
||||
* what is inside the `latest` key.
|
||||
*
|
||||
* If the `size` is bigger than 1, it means it should be rendered as a folder.
|
||||
* In those cases we add `isFolder` key in order to render it properly.
|
||||
*
|
||||
* @param {Array} environments
|
||||
* @returns {Array}
|
||||
*/
|
||||
storeEnvironments(environments = []) {
|
||||
const filteredEnvironments = environments.map((env) => {
|
||||
let filtered = {};
|
||||
|
||||
return this;
|
||||
},
|
||||
if (env.size > 1) {
|
||||
filtered = Object.assign({}, env, { isFolder: true, folderName: env.name });
|
||||
}
|
||||
|
||||
/**
|
||||
* In order to display a tree view we need to modify the received
|
||||
* data in to a tree structure based on `environment_type`
|
||||
* sorted alphabetically.
|
||||
* In each children a `vue-` property will be added. This property will be
|
||||
* used to know if an item is a children mostly for css purposes. This is
|
||||
* needed because the children row is a fragment instance and therfore does
|
||||
* not accept non-prop attributes.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* it will transform this:
|
||||
* [
|
||||
* { name: "environment", environment_type: "review" },
|
||||
* { name: "environment_1", environment_type: null }
|
||||
* { name: "environment_2, environment_type: "review" }
|
||||
* ]
|
||||
* into this:
|
||||
* [
|
||||
* { name: "review", children:
|
||||
* [
|
||||
* { name: "environment", environment_type: "review", vue-isChildren: true},
|
||||
* { name: "environment_2", environment_type: "review", vue-isChildren: true}
|
||||
* ]
|
||||
* },
|
||||
* {name: "environment_1", environment_type: null}
|
||||
* ]
|
||||
*
|
||||
*
|
||||
* @param {Array} environments List of environments.
|
||||
* @returns {Array} Tree structured array with the received environments.
|
||||
*/
|
||||
storeEnvironments(environments = []) {
|
||||
this.state.stoppedCounter = this.countByState(environments, 'stopped');
|
||||
this.state.availableCounter = this.countByState(environments, 'available');
|
||||
if (env.latest) {
|
||||
filtered = Object.assign(filtered, env, env.latest);
|
||||
delete filtered.latest;
|
||||
} else {
|
||||
filtered = Object.assign(filtered, env);
|
||||
}
|
||||
|
||||
const environmentsTree = environments.reduce((acc, environment) => {
|
||||
if (environment.environment_type !== null) {
|
||||
const occurs = acc.filter(element => element.children &&
|
||||
element.name === environment.environment_type);
|
||||
return filtered;
|
||||
});
|
||||
|
||||
environment['vue-isChildren'] = true;
|
||||
this.state.environments = filteredEnvironments;
|
||||
|
||||
if (occurs.length) {
|
||||
acc[acc.indexOf(occurs[0])].children.push(environment);
|
||||
acc[acc.indexOf(occurs[0])].children.slice().sort(this.sortByName);
|
||||
} else {
|
||||
acc.push({
|
||||
name: environment.environment_type,
|
||||
children: [environment],
|
||||
isOpen: false,
|
||||
'vue-isChildren': environment['vue-isChildren'],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
acc.push(environment);
|
||||
}
|
||||
return filteredEnvironments;
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []).slice().sort(this.sortByName);
|
||||
setPagination(pagination = {}) {
|
||||
const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
|
||||
const paginationInformation = gl.utils.parseIntPagination(normalizedHeaders);
|
||||
|
||||
this.state.environments = environmentsTree;
|
||||
this.state.paginationInformation = paginationInformation;
|
||||
return paginationInformation;
|
||||
}
|
||||
|
||||
this.filterEnvironmentsByVisibility(this.state.environments);
|
||||
/**
|
||||
* Stores the number of available environments.
|
||||
*
|
||||
* @param {Number} count = 0
|
||||
* @return {Number}
|
||||
*/
|
||||
storeAvailableCount(count = 0) {
|
||||
this.state.availableCounter = count;
|
||||
return count;
|
||||
}
|
||||
|
||||
return environmentsTree;
|
||||
},
|
||||
/**
|
||||
* Stores the number of closed environments.
|
||||
*
|
||||
* @param {Number} count = 0
|
||||
* @return {Number}
|
||||
*/
|
||||
storeStoppedCount(count = 0) {
|
||||
this.state.stoppedCounter = count;
|
||||
return count;
|
||||
}
|
||||
}
|
||||
|
||||
storeVisibility(visibility) {
|
||||
this.state.visibility = visibility;
|
||||
},
|
||||
/**
|
||||
* Given the visibility prop provided by the url query parameter and which
|
||||
* changes according to the active tab we need to filter which environments
|
||||
* should be visible.
|
||||
*
|
||||
* The environments array is a recursive tree structure and we need to filter
|
||||
* both root level environments and children environments.
|
||||
*
|
||||
* In order to acomplish that, both `filterState` and `filterEnvironmentsByVisibility`
|
||||
* functions work together.
|
||||
* The first one works as the filter that verifies if the given environment matches
|
||||
* the given state.
|
||||
* The second guarantees both root level and children elements are filtered as well.
|
||||
*
|
||||
* Given array of environments will return only
|
||||
* the environments that match the state stored.
|
||||
*
|
||||
* @param {Array} array
|
||||
* @return {Array}
|
||||
*/
|
||||
filterEnvironmentsByVisibility(arr) {
|
||||
const filteredEnvironments = arr.map((item) => {
|
||||
if (item.children) {
|
||||
const filteredChildren = this.filterEnvironmentsByVisibility(
|
||||
item.children,
|
||||
).filter(Boolean);
|
||||
|
||||
if (filteredChildren.length) {
|
||||
item.children = filteredChildren;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
return this.filterState(this.state.visibility, item);
|
||||
}).filter(Boolean);
|
||||
|
||||
this.state.filteredEnvironments = filteredEnvironments;
|
||||
return filteredEnvironments;
|
||||
},
|
||||
|
||||
/**
|
||||
* Given the state and the environment,
|
||||
* returns only if the environment state matches the one provided.
|
||||
*
|
||||
* @param {String} state
|
||||
* @param {Object} environment
|
||||
* @return {Object}
|
||||
*/
|
||||
filterState(state, environment) {
|
||||
return environment.state === state && environment;
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles folder open property given the environment type.
|
||||
*
|
||||
* @param {String} envType
|
||||
* @return {Array}
|
||||
*/
|
||||
toggleFolder(envType) {
|
||||
const environments = this.state.environments;
|
||||
|
||||
const environmentsCopy = environments.map((env) => {
|
||||
if (env['vue-isChildren'] && env.name === envType) {
|
||||
env.isOpen = !env.isOpen;
|
||||
}
|
||||
|
||||
return env;
|
||||
});
|
||||
|
||||
this.state.environments = environmentsCopy;
|
||||
|
||||
return environmentsCopy;
|
||||
},
|
||||
|
||||
/**
|
||||
* Given an array of environments, returns the number of environments
|
||||
* that have the given state.
|
||||
*
|
||||
* @param {Array} environments
|
||||
* @param {String} state
|
||||
* @returns {Number}
|
||||
*/
|
||||
countByState(environments, state) {
|
||||
return environments.filter(env => env.state === state).length;
|
||||
},
|
||||
|
||||
/**
|
||||
* Sorts the two objects provided by their name.
|
||||
*
|
||||
* @param {Object} a
|
||||
* @param {Object} b
|
||||
* @returns {Number}
|
||||
*/
|
||||
sortByName(a, b) {
|
||||
const nameA = a.name.toUpperCase();
|
||||
const nameB = b.name.toUpperCase();
|
||||
|
||||
return nameA < nameB ? -1 : nameA > nameB ? 1 : 0; // eslint-disable-line
|
||||
},
|
||||
};
|
||||
})();
|
||||
module.exports = EnvironmentsStore;
|
||||
|
|
|
@ -103,6 +103,9 @@
|
|||
this.input.each((i, input) => {
|
||||
const $input = $(input);
|
||||
$input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
|
||||
// This triggers at.js again
|
||||
// Needed for slash commands with suffixes (ex: /label ~)
|
||||
$input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
|
||||
});
|
||||
},
|
||||
setupAtWho: function($input) {
|
||||
|
|
|
@ -47,9 +47,11 @@
|
|||
}
|
||||
// Only filter asynchronously only if option remote is set
|
||||
if (this.options.remote) {
|
||||
$inputContainer.parent().addClass('is-loading');
|
||||
clearTimeout(timeout);
|
||||
return timeout = setTimeout(function() {
|
||||
return this.options.query(this.input.val(), function(data) {
|
||||
$inputContainer.parent().removeClass('is-loading');
|
||||
return this.options.callback(data);
|
||||
}.bind(this));
|
||||
}.bind(this), 250);
|
||||
|
|
|
@ -40,11 +40,12 @@
|
|||
calendar = new Pikaday({
|
||||
field: $issuableDueDate.get(0),
|
||||
theme: 'gitlab-theme',
|
||||
format: 'YYYY-MM-DD',
|
||||
format: 'yyyy-mm-dd',
|
||||
onSelect: function(dateText) {
|
||||
$issuableDueDate.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
|
||||
}
|
||||
});
|
||||
calendar.setDate(new Date($issuableDueDate.val()));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -54,16 +54,19 @@ require('vendor/task_list');
|
|||
success: function(data, textStatus, jqXHR) {
|
||||
if ('id' in data) {
|
||||
$(document).trigger('issuable:change');
|
||||
const currentTotal = Number($('.issue_counter').text());
|
||||
if (isClose) {
|
||||
$('a.btn-close').addClass('hidden');
|
||||
$('a.btn-reopen').removeClass('hidden');
|
||||
$('div.status-box-closed').removeClass('hidden');
|
||||
$('div.status-box-open').addClass('hidden');
|
||||
$('.issue_counter').text(currentTotal - 1);
|
||||
} else {
|
||||
$('a.btn-reopen').addClass('hidden');
|
||||
$('a.btn-close').removeClass('hidden');
|
||||
$('div.status-box-closed').addClass('hidden');
|
||||
$('div.status-box-open').removeClass('hidden');
|
||||
$('.issue_counter').text(currentTotal + 1);
|
||||
}
|
||||
} else {
|
||||
new Flash(issueFailMessage, 'alert');
|
||||
|
|
|
@ -231,6 +231,21 @@
|
|||
return upperCaseHeaders;
|
||||
};
|
||||
|
||||
/**
|
||||
* Parses pagination object string values into numbers.
|
||||
*
|
||||
* @param {Object} paginationInformation
|
||||
* @returns {Object}
|
||||
*/
|
||||
w.gl.utils.parseIntPagination = paginationInformation => ({
|
||||
perPage: parseInt(paginationInformation['X-PER-PAGE'], 10),
|
||||
page: parseInt(paginationInformation['X-PAGE'], 10),
|
||||
total: parseInt(paginationInformation['X-TOTAL'], 10),
|
||||
totalPages: parseInt(paginationInformation['X-TOTAL-PAGES'], 10),
|
||||
nextPage: parseInt(paginationInformation['X-NEXT-PAGE'], 10),
|
||||
previousPage: parseInt(paginationInformation['X-PREV-PAGE'], 10),
|
||||
});
|
||||
|
||||
/**
|
||||
* Transforms a DOMStringMap into a plain object.
|
||||
*
|
||||
|
@ -241,5 +256,45 @@
|
|||
acc[element] = DOMStringMapObject[element];
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
/**
|
||||
* Updates the search parameter of a URL given the parameter and values provided.
|
||||
*
|
||||
* If no search params are present we'll add it.
|
||||
* If param for page is already present, we'll update it
|
||||
* If there are params but not for the given one, we'll add it at the end.
|
||||
* Returns the new search parameters.
|
||||
*
|
||||
* @param {String} param
|
||||
* @param {Number|String|Undefined|Null} value
|
||||
* @return {String}
|
||||
*/
|
||||
w.gl.utils.setParamInURL = (param, value) => {
|
||||
let search;
|
||||
const locationSearch = window.location.search;
|
||||
|
||||
if (locationSearch.length === 0) {
|
||||
search = `?${param}=${value}`;
|
||||
}
|
||||
|
||||
if (locationSearch.indexOf(param) !== -1) {
|
||||
const regex = new RegExp(param + '=\\d');
|
||||
search = locationSearch.replace(regex, `${param}=${value}`);
|
||||
}
|
||||
|
||||
if (locationSearch.length && locationSearch.indexOf(param) === -1) {
|
||||
search = `${locationSearch}&${param}=${value}`;
|
||||
}
|
||||
|
||||
return search;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts permission provided as strings to booleans.
|
||||
*
|
||||
* @param {String} string
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
w.gl.utils.convertPermissionToBoolean = permission => permission === 'true';
|
||||
})(window);
|
||||
}).call(this);
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
const calendar = new Pikaday({
|
||||
field: $input.get(0),
|
||||
theme: 'gitlab-theme',
|
||||
format: 'YYYY-MM-DD',
|
||||
format: 'yyyy-mm-dd',
|
||||
minDate: new Date(),
|
||||
onSelect(dateText) {
|
||||
$input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
|
||||
|
@ -30,6 +30,7 @@
|
|||
},
|
||||
});
|
||||
|
||||
calendar.setDate(new Date($input.val()));
|
||||
$input.data('pikaday', calendar);
|
||||
});
|
||||
|
||||
|
|
|
@ -61,6 +61,7 @@ require('./flash');
|
|||
|
||||
constructor({ action, setUrl, stubLocation } = {}) {
|
||||
this.diffsLoaded = false;
|
||||
this.pipelinesLoaded = false;
|
||||
this.commitsLoaded = false;
|
||||
this.fixedLayoutPref = null;
|
||||
|
||||
|
@ -102,9 +103,10 @@ require('./flash');
|
|||
}
|
||||
|
||||
clickTab(e) {
|
||||
if (e.target && gl.utils.isMetaClick(e)) {
|
||||
const targetLink = e.target.getAttribute('href');
|
||||
if (e.currentTarget && gl.utils.isMetaClick(e)) {
|
||||
const targetLink = e.currentTarget.getAttribute('href');
|
||||
e.stopImmediatePropagation();
|
||||
e.preventDefault();
|
||||
window.open(targetLink, '_blank');
|
||||
}
|
||||
}
|
||||
|
@ -128,6 +130,13 @@ require('./flash');
|
|||
$.scrollTo('.merge-request-details .merge-request-tabs', {
|
||||
offset: 0,
|
||||
});
|
||||
} else if (action === 'pipelines') {
|
||||
if (this.pipelinesLoaded) {
|
||||
return;
|
||||
}
|
||||
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
|
||||
gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl);
|
||||
this.pipelinesLoaded = true;
|
||||
} else {
|
||||
this.expandView();
|
||||
this.resetViewContainer();
|
||||
|
|
|
@ -110,7 +110,7 @@ require('./smart_interval');
|
|||
urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
|
||||
return window.location.href = window.location.pathname + urlSuffix;
|
||||
} else if (data.merge_error) {
|
||||
return _this.$widgetBody.html("<h4>" + data.merge_error + "</h4>");
|
||||
return $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>");
|
||||
} else {
|
||||
callback = function() {
|
||||
return merge_request_widget.mergeInProgress(deleteSourceBranch);
|
||||
|
|
|
@ -29,7 +29,7 @@
|
|||
if (selected.id == null) {
|
||||
return selected.text;
|
||||
} else {
|
||||
return selected.kind + ": " + selected.path;
|
||||
return selected.kind + ": " + selected.full_path;
|
||||
}
|
||||
},
|
||||
data: function(term, dataCallback) {
|
||||
|
@ -50,7 +50,7 @@
|
|||
if (namespace.id == null) {
|
||||
return namespace.text;
|
||||
} else {
|
||||
return namespace.kind + ": " + namespace.path;
|
||||
return namespace.kind + ": " + namespace.full_path;
|
||||
}
|
||||
},
|
||||
renderRow: this.renderRow,
|
||||
|
|
|
@ -923,9 +923,10 @@ require('vendor/task_list');
|
|||
};
|
||||
|
||||
Notes.prototype.toggleCommitList = function(e) {
|
||||
const $element = $(e.target);
|
||||
const $element = $(e.currentTarget);
|
||||
const $closestSystemCommitList = $element.siblings('.system-note-commit-list');
|
||||
|
||||
$element.find('.fa').toggleClass('fa-angle-down').toggleClass('fa-angle-up');
|
||||
$closestSystemCommitList.toggleClass('hide-shade');
|
||||
};
|
||||
|
||||
|
|
|
@ -25,6 +25,7 @@
|
|||
bindEvents() {
|
||||
$('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
|
||||
$('#user_notification_email').on('change', this.submitForm);
|
||||
$('#user_notified_of_own_activity').on('change', this.submitForm);
|
||||
$('.update-username').on('ajax:before', this.beforeUpdateUsername);
|
||||
$('.update-username').on('ajax:complete', this.afterUpdateUsername);
|
||||
$('.update-notifications').on('ajax:success', this.onUpdateNotifs);
|
||||
|
|
|
@ -38,13 +38,15 @@
|
|||
this.$buttons.attr('data-status', newStatus);
|
||||
this.$buttons.find('> span').text(newAction);
|
||||
|
||||
for (const button of this.$buttons) {
|
||||
this.$buttons.map((button) => {
|
||||
const $button = $(button);
|
||||
|
||||
if ($button.attr('data-original-title')) {
|
||||
$button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle');
|
||||
}
|
||||
}
|
||||
|
||||
return button;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,11 +21,16 @@
|
|||
};
|
||||
|
||||
Sidebar.prototype.addEventListeners = function() {
|
||||
const $document = $(document);
|
||||
const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight, 10);
|
||||
|
||||
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
|
||||
$('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
|
||||
$('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
|
||||
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
|
||||
$(document).on('click', '.js-sidebar-toggle', function(e, triggered) {
|
||||
$(window).on('resize', () => throttledSetSidebarHeight());
|
||||
$document.on('scroll', () => throttledSetSidebarHeight());
|
||||
$document.on('click', '.js-sidebar-toggle', function(e, triggered) {
|
||||
var $allGutterToggleIcons, $this, $thisIcon;
|
||||
e.preventDefault();
|
||||
$this = $(this);
|
||||
|
@ -191,6 +196,17 @@
|
|||
}
|
||||
};
|
||||
|
||||
Sidebar.prototype.setSidebarHeight = function() {
|
||||
const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight();
|
||||
const $rightSidebar = $('.js-right-sidebar');
|
||||
const diff = $navHeight - $('body').scrollTop();
|
||||
if (diff > 0) {
|
||||
$rightSidebar.outerHeight($(window).height() - diff);
|
||||
} else {
|
||||
$rightSidebar.outerHeight('100%');
|
||||
}
|
||||
};
|
||||
|
||||
Sidebar.prototype.isOpen = function() {
|
||||
return this.sidebar.is('.right-sidebar-expanded');
|
||||
};
|
||||
|
|
|
@ -1,111 +0,0 @@
|
|||
/* eslint-disable arrow-parens, class-methods-use-this, no-param-reassign */
|
||||
/* global Cookies */
|
||||
|
||||
(() => {
|
||||
const pinnedStateCookie = 'pin_nav';
|
||||
const sidebarBreakpoint = 1024;
|
||||
|
||||
const pageSelector = '.page-with-sidebar';
|
||||
const navbarSelector = '.navbar-gitlab';
|
||||
const sidebarWrapperSelector = '.sidebar-wrapper';
|
||||
const sidebarContentSelector = '.nav-sidebar';
|
||||
|
||||
const pinnedToggleSelector = '.js-nav-pin';
|
||||
const sidebarToggleSelector = '.toggle-nav-collapse, .side-nav-toggle';
|
||||
|
||||
const pinnedPageClass = 'page-sidebar-pinned';
|
||||
const expandedPageClass = 'page-sidebar-expanded';
|
||||
|
||||
const pinnedNavbarClass = 'header-sidebar-pinned';
|
||||
const expandedNavbarClass = 'header-sidebar-expanded';
|
||||
|
||||
class Sidebar {
|
||||
constructor() {
|
||||
if (!Sidebar.singleton) {
|
||||
Sidebar.singleton = this;
|
||||
Sidebar.singleton.init();
|
||||
}
|
||||
|
||||
return Sidebar.singleton;
|
||||
}
|
||||
|
||||
init() {
|
||||
this.isPinned = Cookies.get(pinnedStateCookie) === 'true';
|
||||
this.isExpanded = (
|
||||
window.innerWidth >= sidebarBreakpoint &&
|
||||
$(pageSelector).hasClass(expandedPageClass)
|
||||
);
|
||||
$(window).on('resize', () => this.setSidebarHeight());
|
||||
$(document)
|
||||
.on('click', sidebarToggleSelector, () => this.toggleSidebar())
|
||||
.on('click', pinnedToggleSelector, () => this.togglePinnedState())
|
||||
.on('click', 'html, body, a, button', (e) => this.handleClickEvent(e))
|
||||
.on('DOMContentLoaded', () => this.renderState())
|
||||
.on('scroll', () => this.setSidebarHeight())
|
||||
.on('todo:toggle', (e, count) => this.updateTodoCount(count));
|
||||
this.renderState();
|
||||
this.setSidebarHeight();
|
||||
}
|
||||
|
||||
handleClickEvent(e) {
|
||||
if (this.isExpanded && (!this.isPinned || window.innerWidth < sidebarBreakpoint)) {
|
||||
const $target = $(e.target);
|
||||
const targetIsToggle = $target.closest(sidebarToggleSelector).length > 0;
|
||||
const targetIsSidebar = $target.closest(sidebarWrapperSelector).length > 0;
|
||||
if (!targetIsToggle && (!targetIsSidebar || $target.closest('a'))) {
|
||||
this.toggleSidebar();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateTodoCount(count) {
|
||||
$('.js-todos-count').text(gl.text.addDelimiter(count));
|
||||
}
|
||||
|
||||
toggleSidebar() {
|
||||
this.isExpanded = !this.isExpanded;
|
||||
this.renderState();
|
||||
}
|
||||
|
||||
setSidebarHeight() {
|
||||
const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight();
|
||||
const diff = $navHeight - $('body').scrollTop();
|
||||
if (diff > 0) {
|
||||
$('.js-right-sidebar').outerHeight($(window).height() - diff);
|
||||
} else {
|
||||
$('.js-right-sidebar').outerHeight('100%');
|
||||
}
|
||||
}
|
||||
|
||||
togglePinnedState() {
|
||||
this.isPinned = !this.isPinned;
|
||||
if (!this.isPinned) {
|
||||
this.isExpanded = false;
|
||||
}
|
||||
Cookies.set(pinnedStateCookie, this.isPinned ? 'true' : 'false', { expires: 3650 });
|
||||
this.renderState();
|
||||
}
|
||||
|
||||
renderState() {
|
||||
$(pageSelector)
|
||||
.toggleClass(pinnedPageClass, this.isPinned && this.isExpanded)
|
||||
.toggleClass(expandedPageClass, this.isExpanded);
|
||||
$(navbarSelector)
|
||||
.toggleClass(pinnedNavbarClass, this.isPinned && this.isExpanded)
|
||||
.toggleClass(expandedNavbarClass, this.isExpanded);
|
||||
|
||||
const $pinnedToggle = $(pinnedToggleSelector);
|
||||
const tooltipText = this.isPinned ? 'Unpin navigation' : 'Pin navigation';
|
||||
const tooltipState = $pinnedToggle.attr('aria-describedby') && this.isExpanded ? 'show' : 'hide';
|
||||
$pinnedToggle.attr('title', tooltipText).tooltip('fixTitle').tooltip(tooltipState);
|
||||
|
||||
if (this.isExpanded) {
|
||||
const sidebarContent = $(sidebarContentSelector);
|
||||
setTimeout(() => { sidebarContent.niceScroll().updateScrollBar(); }, 200);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
window.gl = window.gl || {};
|
||||
gl.Sidebar = Sidebar;
|
||||
})();
|
|
@ -147,24 +147,21 @@
|
|||
|
||||
goToTodoUrl(e) {
|
||||
const todoLink = this.dataset.url;
|
||||
let targetLink = e.target.getAttribute('href');
|
||||
|
||||
if (e.target.tagName === 'IMG') { // See if clicked target was Avatar
|
||||
targetLink = e.target.parentElement.getAttribute('href'); // Parent of Avatar is link
|
||||
}
|
||||
|
||||
if (!todoLink) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (gl.utils.isMetaClick(e)) {
|
||||
const windowTarget = '_blank';
|
||||
const selected = e.target;
|
||||
e.preventDefault();
|
||||
// Meta-Click on username leads to different URL than todoLink.
|
||||
// Turbolinks can resolve that URL, but window.open requires URL manually.
|
||||
if (targetLink !== todoLink) {
|
||||
return window.open(targetLink, '_blank');
|
||||
|
||||
if (selected.tagName === 'IMG') {
|
||||
const avatarUrl = selected.parentElement.getAttribute('href');
|
||||
return window.open(avatarUrl, windowTarget);
|
||||
} else {
|
||||
return window.open(todoLink, '_blank');
|
||||
return window.open(todoLink, windowTarget);
|
||||
}
|
||||
} else {
|
||||
return gl.utils.visitUrl(todoLink);
|
||||
|
|
|
@ -62,6 +62,7 @@
|
|||
<li v-for='artifact in pipeline.details.artifacts'>
|
||||
<a
|
||||
rel="nofollow"
|
||||
download
|
||||
:href='artifact.path'
|
||||
>
|
||||
<i class="fa fa-download" aria-hidden="true"></i>
|
||||
|
|
|
@ -5,6 +5,7 @@ window.Vue = require('vue');
|
|||
require('../vue_shared/components/table_pagination');
|
||||
require('./store');
|
||||
require('../vue_shared/components/pipelines_table');
|
||||
const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_store');
|
||||
|
||||
((gl) => {
|
||||
gl.VuePipelines = Vue.extend({
|
||||
|
@ -28,15 +29,34 @@ require('../vue_shared/components/pipelines_table');
|
|||
},
|
||||
props: ['scope', 'store', 'svgs'],
|
||||
created() {
|
||||
const pagenum = gl.utils.getParameterByName('p');
|
||||
const pagenum = gl.utils.getParameterByName('page');
|
||||
const scope = gl.utils.getParameterByName('scope');
|
||||
if (pagenum) this.pagenum = pagenum;
|
||||
if (scope) this.apiScope = scope;
|
||||
|
||||
this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope);
|
||||
},
|
||||
|
||||
beforeUpdate() {
|
||||
if (this.pipelines.length && this.$children) {
|
||||
CommitPipelinesStoreWithTimeAgo.startTimeAgoLoops.call(this, Vue);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
/**
|
||||
* Changes the URL according to the pagination component.
|
||||
*
|
||||
* If no scope is provided, 'all' is assumed.
|
||||
*
|
||||
* Pagination component sends "null" when no scope is provided.
|
||||
*
|
||||
* @param {Number} pagenum
|
||||
* @param {String} apiScope = 'all'
|
||||
*/
|
||||
change(pagenum, apiScope) {
|
||||
gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`);
|
||||
if (!apiScope) apiScope = 'all';
|
||||
gl.utils.visitUrl(`?scope=${apiScope}&page=${pagenum}`);
|
||||
},
|
||||
},
|
||||
template: `
|
||||
|
|
|
@ -1,68 +1,31 @@
|
|||
/* global gl, Flash */
|
||||
/* eslint-disable no-param-reassign, no-underscore-dangle */
|
||||
require('../vue_realtime_listener');
|
||||
/* eslint-disable no-param-reassign */
|
||||
|
||||
((gl) => {
|
||||
const pageValues = (headers) => {
|
||||
const normalized = gl.utils.normalizeHeaders(headers);
|
||||
|
||||
const paginationInfo = {
|
||||
perPage: +normalized['X-PER-PAGE'],
|
||||
page: +normalized['X-PAGE'],
|
||||
total: +normalized['X-TOTAL'],
|
||||
totalPages: +normalized['X-TOTAL-PAGES'],
|
||||
nextPage: +normalized['X-NEXT-PAGE'],
|
||||
previousPage: +normalized['X-PREV-PAGE'],
|
||||
};
|
||||
|
||||
const paginationInfo = gl.utils.parseIntPagination(normalized);
|
||||
return paginationInfo;
|
||||
};
|
||||
|
||||
gl.PipelineStore = class {
|
||||
fetchDataLoop(Vue, pageNum, url, apiScope) {
|
||||
this.pageRequest = true;
|
||||
const updatePipelineNums = (count) => {
|
||||
const { all } = count;
|
||||
const running = count.running_or_pending;
|
||||
document.querySelector('.js-totalbuilds-count').innerHTML = all;
|
||||
document.querySelector('.js-running-count').innerHTML = running;
|
||||
};
|
||||
|
||||
const goFetch = () =>
|
||||
this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`)
|
||||
.then((response) => {
|
||||
const pageInfo = pageValues(response.headers);
|
||||
this.pageInfo = Object.assign({}, this.pageInfo, pageInfo);
|
||||
return this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`)
|
||||
.then((response) => {
|
||||
const pageInfo = pageValues(response.headers);
|
||||
this.pageInfo = Object.assign({}, this.pageInfo, pageInfo);
|
||||
|
||||
const res = JSON.parse(response.body);
|
||||
this.count = Object.assign({}, this.count, res.count);
|
||||
this.pipelines = Object.assign([], this.pipelines, res.pipelines);
|
||||
const res = JSON.parse(response.body);
|
||||
this.count = Object.assign({}, this.count, res.count);
|
||||
this.pipelines = Object.assign([], this.pipelines, res.pipelines);
|
||||
|
||||
updatePipelineNums(this.count);
|
||||
this.pageRequest = false;
|
||||
}, () => {
|
||||
this.pageRequest = false;
|
||||
return new Flash('An error occurred while fetching the pipelines, please reload the page again.');
|
||||
});
|
||||
|
||||
goFetch();
|
||||
|
||||
const startTimeLoops = () => {
|
||||
this.timeLoopInterval = setInterval(() => {
|
||||
this.$children[0].$children.reduce((acc, component) => {
|
||||
const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0];
|
||||
acc.push(timeAgoComponent);
|
||||
return acc;
|
||||
}, []).forEach(e => e.changeTime());
|
||||
}, 10000);
|
||||
};
|
||||
|
||||
startTimeLoops();
|
||||
|
||||
const removeIntervals = () => clearInterval(this.timeLoopInterval);
|
||||
const startIntervals = () => startTimeLoops();
|
||||
|
||||
gl.VueRealtimeListener(removeIntervals, startIntervals);
|
||||
this.pageRequest = false;
|
||||
}, () => {
|
||||
this.pageRequest = false;
|
||||
return new Flash('An error occurred while fetching the pipelines, please reload the page again.');
|
||||
});
|
||||
}
|
||||
};
|
||||
})(window.gl || (window.gl = {}));
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/* global Vue */
|
||||
window.Vue = require('vue');
|
||||
|
||||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
|
|
|
@ -57,9 +57,7 @@ window.Vue = require('vue');
|
|||
},
|
||||
methods: {
|
||||
changePage(e) {
|
||||
let apiScope = gl.utils.getParameterByName('scope');
|
||||
|
||||
if (!apiScope) apiScope = 'all';
|
||||
const apiScope = gl.utils.getParameterByName('scope');
|
||||
|
||||
const text = e.target.innerText;
|
||||
const { totalPages, nextPage, previousPage } = this.pageInfo;
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
@import "framework/flash.scss";
|
||||
@import "framework/forms.scss";
|
||||
@import "framework/gfm.scss";
|
||||
@import "framework/gitlab-theme.scss";
|
||||
@import "framework/header.scss";
|
||||
@import "framework/highlight.scss";
|
||||
@import "framework/issue_box.scss";
|
||||
|
|
|
@ -116,7 +116,7 @@
|
|||
}
|
||||
|
||||
.btn,
|
||||
.side-nav-toggle {
|
||||
.global-dropdown-toggle {
|
||||
@include transition(background-color, border-color, color, box-shadow);
|
||||
}
|
||||
|
||||
|
@ -128,8 +128,7 @@
|
|||
|
||||
.note-action-button .link-highlight,
|
||||
.toolbar-btn,
|
||||
.dropdown-toggle-caret,
|
||||
.fa:not(.fa-bell) {
|
||||
.dropdown-toggle-caret {
|
||||
@include transition(color);
|
||||
}
|
||||
|
||||
|
@ -141,7 +140,6 @@ a {
|
|||
@include transition(background-color, box-shadow);
|
||||
}
|
||||
|
||||
.nav-sidebar a,
|
||||
.dropdown-menu a,
|
||||
.dropdown-menu button,
|
||||
.dropdown-menu-nav a {
|
||||
|
|
|
@ -28,6 +28,8 @@
|
|||
|
||||
.avatar {
|
||||
@extend .avatar-circle;
|
||||
@include transition-property(none);
|
||||
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
padding: 0;
|
||||
|
|
|
@ -1,144 +0,0 @@
|
|||
/**
|
||||
* Styles the GitLab application with a specific color theme
|
||||
*
|
||||
* $color-light -
|
||||
* $color -
|
||||
* $color-darker -
|
||||
* $color-dark -
|
||||
*/
|
||||
@mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) {
|
||||
.page-with-sidebar {
|
||||
.toggle-nav-collapse,
|
||||
.pin-nav-btn {
|
||||
color: $color-light;
|
||||
|
||||
&:hover {
|
||||
color: $white-light;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-wrapper {
|
||||
background: $color-darker;
|
||||
}
|
||||
|
||||
.sidebar-action-buttons {
|
||||
color: $color-light;
|
||||
background-color: lighten($color-darker, 5%);
|
||||
}
|
||||
|
||||
.nav-sidebar {
|
||||
li {
|
||||
a {
|
||||
color: $color-light;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&:active {
|
||||
background: $color-dark;
|
||||
}
|
||||
|
||||
i {
|
||||
color: $color-light;
|
||||
}
|
||||
|
||||
path,
|
||||
polygon {
|
||||
fill: $color-light;
|
||||
}
|
||||
|
||||
.count {
|
||||
color: $color-light;
|
||||
background: $color-dark;
|
||||
}
|
||||
|
||||
svg {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
&.separate-item {
|
||||
border-top: 1px solid $color;
|
||||
}
|
||||
|
||||
&.active a {
|
||||
color: $white-light;
|
||||
background: $color-dark;
|
||||
|
||||
&.no-highlight {
|
||||
border: none;
|
||||
}
|
||||
|
||||
i {
|
||||
color: $white-light;
|
||||
}
|
||||
|
||||
path,
|
||||
polygon {
|
||||
fill: $white-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.about-gitlab {
|
||||
color: $color-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$theme-charcoal-light: #b9bbbe;
|
||||
$theme-charcoal: #485157;
|
||||
$theme-charcoal-dark: #3d454d;
|
||||
$theme-charcoal-darker: #383f45;
|
||||
|
||||
$theme-blue-light: #becde9;
|
||||
$theme-blue: #2980b9;
|
||||
$theme-blue-dark: #1970a9;
|
||||
$theme-blue-darker: #096099;
|
||||
|
||||
$theme-graphite-light: #ccc;
|
||||
$theme-graphite: #777;
|
||||
$theme-graphite-dark: #666;
|
||||
$theme-graphite-darker: #555;
|
||||
|
||||
$theme-black-light: #979797;
|
||||
$theme-black: #373737;
|
||||
$theme-black-dark: #272727;
|
||||
$theme-black-darker: #222;
|
||||
|
||||
$theme-green-light: #adc;
|
||||
$theme-green: #019875;
|
||||
$theme-green-dark: #018865;
|
||||
$theme-green-darker: #017855;
|
||||
|
||||
$theme-violet-light: #98c;
|
||||
$theme-violet: #548;
|
||||
$theme-violet-dark: #436;
|
||||
$theme-violet-darker: #325;
|
||||
|
||||
body {
|
||||
&.ui_blue {
|
||||
@include gitlab-theme($theme-blue-light, $theme-blue, $theme-blue-dark, $theme-blue-darker);
|
||||
}
|
||||
|
||||
&.ui_charcoal {
|
||||
@include gitlab-theme($theme-charcoal-light, $theme-charcoal, $theme-charcoal-dark, $theme-charcoal-darker);
|
||||
}
|
||||
|
||||
&.ui_graphite {
|
||||
@include gitlab-theme($theme-graphite-light, $theme-graphite, $theme-graphite-dark, $theme-graphite-darker);
|
||||
}
|
||||
|
||||
&.ui_black {
|
||||
@include gitlab-theme($theme-black-light, $theme-black, $theme-black-dark, $theme-black-darker);
|
||||
}
|
||||
|
||||
&.ui_green {
|
||||
@include gitlab-theme($theme-green-light, $theme-green, $theme-green-dark, $theme-green-darker);
|
||||
}
|
||||
|
||||
&.ui_violet {
|
||||
@include gitlab-theme($theme-violet-light, $theme-violet, $theme-violet-dark, $theme-violet-darker);
|
||||
}
|
||||
}
|
|
@ -100,23 +100,42 @@ header {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.side-nav-toggle {
|
||||
position: absolute;
|
||||
left: -10px;
|
||||
margin: 7px 0;
|
||||
font-size: 18px;
|
||||
padding: 6px 10px;
|
||||
border: none;
|
||||
background-color: $gray-light;
|
||||
.global-dropdown {
|
||||
position: absolute;
|
||||
left: -10px;
|
||||
|
||||
.badge {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
li {
|
||||
.active a {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background-color: $white-normal;
|
||||
color: $gl-header-nav-hover-color;
|
||||
.badge {
|
||||
background-color: $white-light;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.global-dropdown-toggle {
|
||||
margin: 7px 0;
|
||||
font-size: 18px;
|
||||
padding: 6px 10px;
|
||||
border: none;
|
||||
background-color: $gray-light;
|
||||
|
||||
&:hover {
|
||||
background-color: $white-normal;
|
||||
color: $gl-header-nav-hover-color;
|
||||
}
|
||||
}
|
||||
|
||||
.header-content {
|
||||
position: relative;
|
||||
height: $header-height;
|
||||
|
|
|
@ -1,36 +1,3 @@
|
|||
.page-with-sidebar {
|
||||
padding-bottom: 25px;
|
||||
transition: padding $sidebar-transition-duration;
|
||||
|
||||
&.page-sidebar-pinned {
|
||||
.sidebar-wrapper {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-wrapper {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 0;
|
||||
overflow: hidden;
|
||||
transition: width $sidebar-transition-duration;
|
||||
box-shadow: 2px 0 16px 0 $black-transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-wrapper {
|
||||
z-index: 1000;
|
||||
background: $gray-light;
|
||||
|
||||
.nicescroll-rails-hr {
|
||||
// TODO: Figure out why nicescroll doesn't hide horizontal bar
|
||||
display: none!important;
|
||||
}
|
||||
}
|
||||
|
||||
.content-wrapper {
|
||||
width: 100%;
|
||||
transition: padding $sidebar-transition-duration;
|
||||
|
@ -47,105 +14,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.nav-sidebar {
|
||||
position: absolute;
|
||||
top: 50px;
|
||||
bottom: 0;
|
||||
width: $sidebar_width;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
|
||||
&.navbar-collapse {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
li {
|
||||
&.separate-item {
|
||||
padding-top: 10px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
width: 34px;
|
||||
display: inline-block;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
a {
|
||||
padding: 7px $gl-sidebar-padding;
|
||||
font-size: $gl-font-size;
|
||||
line-height: 24px;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
font-weight: normal;
|
||||
|
||||
&:hover,
|
||||
&:active,
|
||||
&:focus {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
i {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
i,
|
||||
svg {
|
||||
margin-right: 13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.count {
|
||||
float: right;
|
||||
padding: 0 8px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.about-gitlab {
|
||||
padding: 7px $gl-sidebar-padding;
|
||||
font-size: $gl-font-size;
|
||||
line-height: 24px;
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
font-weight: normal;
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-action-buttons {
|
||||
width: $sidebar_width;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
min-height: 50px;
|
||||
padding: 5px 0;
|
||||
font-size: 18px;
|
||||
line-height: 30px;
|
||||
|
||||
.toggle-nav-collapse {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.pin-nav-btn {
|
||||
right: 0;
|
||||
display: none;
|
||||
|
||||
@media (min-width: $sidebar-breakpoint) {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.fa {
|
||||
transition: transform .15s;
|
||||
|
||||
.page-sidebar-pinned & {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-header-btn {
|
||||
padding: 10px $gl-sidebar-padding;
|
||||
color: inherit;
|
||||
|
@ -161,59 +29,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
.page-sidebar-expanded {
|
||||
.sidebar-wrapper {
|
||||
width: $sidebar_width;
|
||||
}
|
||||
}
|
||||
|
||||
.page-sidebar-pinned {
|
||||
.content-wrapper,
|
||||
.layout-nav {
|
||||
@media (min-width: $sidebar-breakpoint) {
|
||||
padding-left: $sidebar_width;
|
||||
}
|
||||
}
|
||||
|
||||
.merge-request-tabs-holder.affix {
|
||||
@media (min-width: $sidebar-breakpoint) {
|
||||
left: $sidebar_width;
|
||||
}
|
||||
}
|
||||
|
||||
&.right-sidebar-expanded {
|
||||
.line-resolve-all-container {
|
||||
@media (min-width: $sidebar-breakpoint) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
header.header-sidebar-pinned {
|
||||
@media (min-width: $sidebar-breakpoint) {
|
||||
padding-left: ($sidebar_width + $gl-padding);
|
||||
|
||||
.side-nav-toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.right-sidebar-collapsed {
|
||||
padding-right: 0;
|
||||
|
||||
@media (min-width: $screen-sm-min) {
|
||||
.content-wrapper {
|
||||
padding-right: $sidebar_collapsed_width;
|
||||
padding-right: $gutter_collapsed_width;
|
||||
}
|
||||
|
||||
.merge-request-tabs-holder.affix {
|
||||
right: $sidebar_collapsed_width;
|
||||
right: $gutter_collapsed_width;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -231,7 +56,7 @@ header.header-sidebar-pinned {
|
|||
|
||||
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
|
||||
&:not(.build-sidebar):not(.wiki-sidebar) {
|
||||
padding-right: $sidebar_collapsed_width;
|
||||
padding-right: $gutter_collapsed_width;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -245,12 +70,12 @@ header.header-sidebar-pinned {
|
|||
}
|
||||
|
||||
&.with-overlay .merge-request-tabs-holder.affix {
|
||||
right: $sidebar_collapsed_width;
|
||||
right: $gutter_collapsed_width;
|
||||
}
|
||||
}
|
||||
|
||||
&.with-overlay {
|
||||
padding-right: $sidebar_collapsed_width;
|
||||
padding-right: $gutter_collapsed_width;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
/*
|
||||
* Layout
|
||||
*/
|
||||
$sidebar_collapsed_width: 62px;
|
||||
$sidebar_width: 220px;
|
||||
$gutter_collapsed_width: 62px;
|
||||
$gutter_width: 290px;
|
||||
$gutter_inner_width: 250px;
|
||||
|
@ -541,4 +539,4 @@ Pipeline Graph
|
|||
*/
|
||||
$stage-hover-bg: #eaf3fc;
|
||||
$stage-hover-border: #d1e7fc;
|
||||
$action-icon-color: #d6d6d6;
|
||||
$action-icon-color: #d6d6d6;
|
||||
|
|
|
@ -91,7 +91,7 @@
|
|||
}
|
||||
|
||||
&.scroll-top {
|
||||
top: 110px;
|
||||
top: 10px;
|
||||
}
|
||||
|
||||
&.scroll-bottom {
|
||||
|
|
|
@ -284,7 +284,11 @@
|
|||
|
||||
.events-description {
|
||||
line-height: 65px;
|
||||
padding-left: $gl-padding;
|
||||
padding: 0 $gl-padding;
|
||||
}
|
||||
|
||||
.events-info {
|
||||
color: $gl-text-color-secondary;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,11 @@
|
|||
font-size: 34px;
|
||||
}
|
||||
|
||||
.environments-folder-name {
|
||||
font-weight: normal;
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: $screen-xs-max) {
|
||||
.environments-container {
|
||||
width: 100%;
|
||||
|
@ -110,17 +115,20 @@
|
|||
}
|
||||
}
|
||||
|
||||
.children-row .environment-name {
|
||||
margin-left: 17px;
|
||||
margin-right: -17px;
|
||||
}
|
||||
|
||||
.folder-icon {
|
||||
padding: 0 5px 0 0;
|
||||
margin-right: 3px;
|
||||
color: $gl-text-color-secondary;
|
||||
display: inline-block;
|
||||
|
||||
.fa:nth-child(1) {
|
||||
margin-right: 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.folder-name {
|
||||
cursor: pointer;
|
||||
color: $gl-text-color-secondary;
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -135,4 +143,4 @@
|
|||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,7 +41,6 @@
|
|||
word-wrap: break-word;
|
||||
|
||||
.md {
|
||||
color: $gl-grayish-blue;
|
||||
font-size: $gl-font-size;
|
||||
|
||||
.label {
|
||||
|
|
|
@ -193,7 +193,6 @@
|
|||
top: $header-height;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
z-index: 8;
|
||||
transition: width .3s;
|
||||
background: $gray-light;
|
||||
padding: 10px 20px;
|
||||
|
@ -254,11 +253,11 @@
|
|||
display: block;
|
||||
}
|
||||
|
||||
width: $sidebar_collapsed_width;
|
||||
width: $gutter_collapsed_width;
|
||||
padding-top: 0;
|
||||
|
||||
.block {
|
||||
width: $sidebar_collapsed_width - 2px;
|
||||
width: $gutter_collapsed_width - 2px;
|
||||
margin-left: -19px;
|
||||
padding: 15px 0 0;
|
||||
border-bottom: none;
|
||||
|
|
|
@ -85,14 +85,18 @@
|
|||
-webkit-align-items: center;
|
||||
align-items: center;
|
||||
|
||||
i,
|
||||
svg {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
svg {
|
||||
margin-right: 4px;
|
||||
position: relative;
|
||||
top: 1px;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
&> span {
|
||||
& > span {
|
||||
padding-right: 4px;
|
||||
}
|
||||
|
||||
|
|
|
@ -72,6 +72,7 @@ ul.notes {
|
|||
overflow: hidden;
|
||||
|
||||
.system-note-commit-list-toggler {
|
||||
color: $gl-link-color;
|
||||
display: none;
|
||||
padding: 10px 0 0;
|
||||
cursor: pointer;
|
||||
|
@ -107,16 +108,6 @@ ul.notes {
|
|||
display: none;
|
||||
}
|
||||
|
||||
p:last-child {
|
||||
a {
|
||||
color: $gl-text-color;
|
||||
|
||||
&:hover {
|
||||
color: $gl-link-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
width: 100%;
|
||||
|
|
|
@ -864,7 +864,7 @@
|
|||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
width: 90px;
|
||||
max-width: 70%;
|
||||
color: $gl-text-color-secondary;
|
||||
margin-left: 2px;
|
||||
display: inline-block;
|
||||
|
|
|
@ -1,42 +1,3 @@
|
|||
.application-theme {
|
||||
label {
|
||||
margin-right: 20px;
|
||||
text-align: center;
|
||||
|
||||
.preview {
|
||||
border-radius: 4px;
|
||||
|
||||
height: 80px;
|
||||
margin-bottom: 10px;
|
||||
width: 160px;
|
||||
|
||||
&.ui_blue {
|
||||
background: $theme-blue;
|
||||
}
|
||||
|
||||
&.ui_charcoal {
|
||||
background: $theme-charcoal;
|
||||
}
|
||||
|
||||
&.ui_graphite {
|
||||
background: $theme-graphite;
|
||||
}
|
||||
|
||||
&.ui_black {
|
||||
background: $theme-black;
|
||||
}
|
||||
|
||||
&.ui_green {
|
||||
background: $theme-green;
|
||||
}
|
||||
|
||||
&.ui_violet {
|
||||
background: $theme-violet;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.syntax-theme {
|
||||
label {
|
||||
margin-right: 20px;
|
||||
|
|
|
@ -35,12 +35,8 @@
|
|||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.project-path {
|
||||
padding-right: 0;
|
||||
|
||||
.form-control {
|
||||
border-radius: $border-radius-base;
|
||||
}
|
||||
.project-path .form-control {
|
||||
border-radius: $border-radius-base;
|
||||
}
|
||||
|
||||
.input-group > div {
|
||||
|
@ -106,6 +102,7 @@
|
|||
font-size: 24px;
|
||||
font-weight: 400;
|
||||
line-height: 1;
|
||||
word-wrap: break-word;
|
||||
|
||||
.fa {
|
||||
margin-left: 2px;
|
||||
|
|
|
@ -171,6 +171,8 @@
|
|||
.tree-controls {
|
||||
float: right;
|
||||
margin-top: 11px;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
.project-action-button {
|
||||
margin-left: $btn-side-margin;
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
.new-wiki-page {
|
||||
.new-wiki-page-slug-tip {
|
||||
display: inline-block;
|
||||
max-width: 100%;
|
||||
margin-top: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.title .edit-wiki-header {
|
||||
width: 780px;
|
||||
margin-left: auto;
|
||||
|
@ -9,12 +17,18 @@
|
|||
@extend .top-area;
|
||||
position: relative;
|
||||
|
||||
.wiki-breadcrumb {
|
||||
border-bottom: 1px solid $white-normal;
|
||||
padding: 11px 0;
|
||||
}
|
||||
|
||||
.wiki-page-title {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
.wiki-last-edit-by {
|
||||
display: block;
|
||||
color: $gl-text-color-secondary;
|
||||
|
||||
strong {
|
||||
|
@ -121,6 +135,10 @@
|
|||
margin: 5px 0 10px;
|
||||
}
|
||||
|
||||
ul.wiki-pages ul {
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
.wiki-sidebar-header {
|
||||
padding: 0 $gl-padding $gl-padding;
|
||||
|
||||
|
@ -129,3 +147,15 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul.wiki-pages-list.content-list {
|
||||
& ul {
|
||||
list-style: none;
|
||||
margin-left: 0;
|
||||
padding-left: 15px;
|
||||
}
|
||||
|
||||
& ul li {
|
||||
padding: 5px 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,6 @@ nav.navbar-collapse.collapse,
|
|||
.blob-commit-info,
|
||||
.file-title,
|
||||
.file-holder,
|
||||
.sidebar-wrapper,
|
||||
.nav,
|
||||
.btn,
|
||||
ul.notes-form,
|
||||
|
|
|
@ -13,7 +13,7 @@ class Admin::RunnersController < Admin::ApplicationController
|
|||
end
|
||||
|
||||
def update
|
||||
if @runner.update_attributes(runner_params)
|
||||
if Ci::UpdateRunnerService.new(@runner).update(runner_params)
|
||||
respond_to do |format|
|
||||
format.js
|
||||
format.html { redirect_to admin_runner_path(@runner) }
|
||||
|
@ -31,7 +31,7 @@ class Admin::RunnersController < Admin::ApplicationController
|
|||
end
|
||||
|
||||
def resume
|
||||
if @runner.update_attributes(active: true)
|
||||
if Ci::UpdateRunnerService.new(@runner).update(active: true)
|
||||
redirect_to admin_runners_path, notice: 'Runner was successfully updated.'
|
||||
else
|
||||
redirect_to admin_runners_path, alert: 'Runner was not updated.'
|
||||
|
@ -39,7 +39,7 @@ class Admin::RunnersController < Admin::ApplicationController
|
|||
end
|
||||
|
||||
def pause
|
||||
if @runner.update_attributes(active: false)
|
||||
if Ci::UpdateRunnerService.new(@runner).update(active: false)
|
||||
redirect_to admin_runners_path, notice: 'Runner was successfully updated.'
|
||||
else
|
||||
redirect_to admin_runners_path, alert: 'Runner was not updated.'
|
||||
|
|
|
@ -194,7 +194,6 @@ class Admin::UsersController < Admin::ApplicationController
|
|||
:provider,
|
||||
:remember_me,
|
||||
:skype,
|
||||
:theme_id,
|
||||
:twitter,
|
||||
:username,
|
||||
:website_url
|
||||
|
|
|
@ -9,6 +9,28 @@ module IssuableCollections
|
|||
|
||||
private
|
||||
|
||||
def issuable_meta_data(issuable_collection)
|
||||
# map has to be used here since using pluck or select will
|
||||
# throw an error when ordering issuables by priority which inserts
|
||||
# a new order into the collection.
|
||||
# We cannot use reorder to not mess up the paginated collection.
|
||||
issuable_ids = issuable_collection.map(&:id)
|
||||
issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type)
|
||||
issuable_votes_count = AwardEmoji.votes_for_collection(issuable_ids, @collection_type)
|
||||
|
||||
issuable_ids.each_with_object({}) do |id, issuable_meta|
|
||||
downvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.downvote? }
|
||||
upvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? }
|
||||
notes = issuable_note_count.find { |notes| notes.noteable_id == id }
|
||||
|
||||
issuable_meta[id] = Issuable::IssuableMeta.new(
|
||||
upvotes.try(:count).to_i,
|
||||
downvotes.try(:count).to_i,
|
||||
notes.try(:count).to_i
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
def issues_collection
|
||||
issues_finder.execute.preload(:project, :author, :assignee, :labels, :milestone, project: :namespace)
|
||||
end
|
||||
|
|
|
@ -9,6 +9,9 @@ module IssuesAction
|
|||
.non_archived
|
||||
.page(params[:page])
|
||||
|
||||
@collection_type = "Issue"
|
||||
@issuable_meta_data = issuable_meta_data(@issues)
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.atom { render layout: false }
|
||||
|
|
|
@ -7,6 +7,9 @@ module MergeRequestsAction
|
|||
|
||||
@merge_requests = merge_requests_collection
|
||||
.page(params[:page])
|
||||
|
||||
@collection_type = "MergeRequest"
|
||||
@issuable_meta_data = issuable_meta_data(@merge_requests)
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
@ -1,19 +1,14 @@
|
|||
class Dashboard::ProjectsController < Dashboard::ApplicationController
|
||||
include FilterProjects
|
||||
|
||||
before_action :event_filter
|
||||
|
||||
def index
|
||||
@projects = current_user.authorized_projects.sorted_by_activity
|
||||
@projects = filter_projects(@projects)
|
||||
@projects = @projects.includes(:namespace)
|
||||
@projects = load_projects(current_user.authorized_projects)
|
||||
@projects = @projects.sort(@sort = params[:sort])
|
||||
@projects = @projects.page(params[:page])
|
||||
|
||||
respond_to do |format|
|
||||
format.html { @last_push = current_user.recent_push }
|
||||
format.atom do
|
||||
event_filter
|
||||
load_events
|
||||
render layout: false
|
||||
end
|
||||
|
@ -26,9 +21,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
|
|||
end
|
||||
|
||||
def starred
|
||||
@projects = current_user.viewable_starred_projects.sorted_by_activity
|
||||
@projects = filter_projects(@projects)
|
||||
@projects = @projects.includes(:namespace, :forked_from_project, :tags)
|
||||
@projects = load_projects(current_user.viewable_starred_projects)
|
||||
@projects = @projects.includes(:forked_from_project, :tags)
|
||||
@projects = @projects.sort(@sort = params[:sort])
|
||||
@projects = @projects.page(params[:page])
|
||||
|
||||
|
@ -37,7 +31,6 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
|
|||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
|
||||
format.json do
|
||||
render json: {
|
||||
html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects })
|
||||
|
@ -48,9 +41,15 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
|
|||
|
||||
private
|
||||
|
||||
def load_projects(base_scope)
|
||||
projects = base_scope.sorted_by_activity.includes(:namespace)
|
||||
|
||||
filter_projects(projects)
|
||||
end
|
||||
|
||||
def load_events
|
||||
@events = Event.in_projects(@projects)
|
||||
@events = @event_filter.apply_filter(@events).with_associations
|
||||
@events = Event.in_projects(load_projects(current_user.authorized_projects))
|
||||
@events = event_filter.apply_filter(@events).with_associations
|
||||
@events = @events.limit(20).offset(params[:offset] || 0)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -17,6 +17,6 @@ class Profiles::NotificationsController < Profiles::ApplicationController
|
|||
end
|
||||
|
||||
def user_params
|
||||
params.require(:user).permit(:notification_email)
|
||||
params.require(:user).permit(:notification_email, :notified_of_own_activity)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -34,7 +34,6 @@ class Profiles::PreferencesController < Profiles::ApplicationController
|
|||
:layout,
|
||||
:dashboard,
|
||||
:project_view,
|
||||
:theme_id
|
||||
)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,15 +9,40 @@ class Projects::EnvironmentsController < Projects::ApplicationController
|
|||
before_action :verify_api_request!, only: :terminal_websocket_authorize
|
||||
|
||||
def index
|
||||
@scope = params[:scope]
|
||||
@environments = project.environments.includes(:last_deployment)
|
||||
@environments = project.environments
|
||||
.with_state(params[:scope] || :available)
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.json do
|
||||
render json: EnvironmentSerializer
|
||||
.new(project: @project, user: current_user)
|
||||
.represent(@environments)
|
||||
render json: {
|
||||
environments: EnvironmentSerializer
|
||||
.new(project: @project, user: @current_user)
|
||||
.with_pagination(request, response)
|
||||
.within_folders
|
||||
.represent(@environments),
|
||||
available_count: project.environments.available.count,
|
||||
stopped_count: project.environments.stopped.count
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def folder
|
||||
folder_environments = project.environments.where(environment_type: params[:id])
|
||||
@environments = folder_environments.with_state(params[:scope] || :available)
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.json do
|
||||
render json: {
|
||||
environments: EnvironmentSerializer
|
||||
.new(project: @project, user: @current_user)
|
||||
.with_pagination(request, response)
|
||||
.represent(@environments),
|
||||
available_count: folder_environments.available.count,
|
||||
stopped_count: folder_environments.stopped.count
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -23,8 +23,11 @@ class Projects::IssuesController < Projects::ApplicationController
|
|||
respond_to :html
|
||||
|
||||
def index
|
||||
@issues = issues_collection
|
||||
@issues = @issues.page(params[:page])
|
||||
@collection_type = "Issue"
|
||||
@issues = issues_collection
|
||||
@issues = @issues.page(params[:page])
|
||||
@issuable_meta_data = issuable_meta_data(@issues)
|
||||
|
||||
if @issues.out_of_range? && @issues.total_pages != 0
|
||||
return redirect_to url_for(params.merge(page: @issues.total_pages))
|
||||
end
|
||||
|
|
|
@ -36,8 +36,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :conflict_for_path, :resolve_conflicts]
|
||||
|
||||
def index
|
||||
@merge_requests = merge_requests_collection
|
||||
@merge_requests = @merge_requests.page(params[:page])
|
||||
@collection_type = "MergeRequest"
|
||||
@merge_requests = merge_requests_collection
|
||||
@merge_requests = @merge_requests.page(params[:page])
|
||||
@issuable_meta_data = issuable_meta_data(@merge_requests)
|
||||
|
||||
if @merge_requests.out_of_range? && @merge_requests.total_pages != 0
|
||||
return redirect_to url_for(params.merge(page: @merge_requests.total_pages))
|
||||
end
|
||||
|
@ -366,10 +369,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def merge_widget_refresh
|
||||
if merge_request.in_progress_merge_commit_sha || merge_request.state == 'merged'
|
||||
@status = :success
|
||||
elsif merge_request.merge_when_build_succeeds
|
||||
if merge_request.merge_when_build_succeeds
|
||||
@status = :merge_when_build_succeeds
|
||||
else
|
||||
# Only MRs that can be merged end in this action
|
||||
# MR can be already picked up for merge / merged already or can be waiting for worker to be picked up
|
||||
# in last case it does not have any special status. Possible error is handled inside widget js function
|
||||
@status = :success
|
||||
end
|
||||
|
||||
render 'merge'
|
||||
|
|
|
@ -12,7 +12,7 @@ class Projects::RunnersController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def update
|
||||
if @runner.update_attributes(runner_params)
|
||||
if Ci::UpdateRunnerService.new(@runner).update(runner_params)
|
||||
redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
|
||||
else
|
||||
render 'edit'
|
||||
|
@ -28,7 +28,7 @@ class Projects::RunnersController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def resume
|
||||
if @runner.update_attributes(active: true)
|
||||
if Ci::UpdateRunnerService.new(@runner).update(active: true)
|
||||
redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
|
||||
else
|
||||
redirect_to runner_path(@runner), alert: 'Runner was not updated.'
|
||||
|
@ -36,7 +36,7 @@ class Projects::RunnersController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def pause
|
||||
if @runner.update_attributes(active: false)
|
||||
if Ci::UpdateRunnerService.new(@runner).update(active: false)
|
||||
redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
|
||||
else
|
||||
redirect_to runner_path(@runner), alert: 'Runner was not updated.'
|
||||
|
|
|
@ -27,7 +27,7 @@ class Projects::TagsController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def create
|
||||
result = CreateTagService.new(@project, current_user).
|
||||
result = Tags::CreateService.new(@project, current_user).
|
||||
execute(params[:tag_name], params[:ref], params[:message], params[:release_description])
|
||||
|
||||
if result[:status] == :success
|
||||
|
@ -41,7 +41,7 @@ class Projects::TagsController < Projects::ApplicationController
|
|||
end
|
||||
|
||||
def destroy
|
||||
DeleteTagService.new(project, current_user).execute(params[:id])
|
||||
Tags::DestroyService.new(project, current_user).execute(params[:id])
|
||||
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
|
|
|
@ -8,6 +8,7 @@ class Projects::WikisController < Projects::ApplicationController
|
|||
|
||||
def pages
|
||||
@wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page])
|
||||
@wiki_entries = WikiPage.group_by_directory(@wiki_pages)
|
||||
end
|
||||
|
||||
def show
|
||||
|
@ -83,7 +84,7 @@ class Projects::WikisController < Projects::ApplicationController
|
|||
|
||||
def destroy
|
||||
@page = @project_wiki.find_page(params[:id])
|
||||
@page.delete if @page
|
||||
WikiPages::DestroyService.new(@project, current_user).execute(@page)
|
||||
|
||||
redirect_to(
|
||||
namespace_project_wiki_path(@project.namespace, @project, :home),
|
||||
|
@ -116,7 +117,7 @@ class Projects::WikisController < Projects::ApplicationController
|
|||
# Call #wiki to make sure the Wiki Repo is initialized
|
||||
@project_wiki.wiki
|
||||
|
||||
@sidebar_wiki_pages = @project_wiki.pages.first(15)
|
||||
@sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages.first(15))
|
||||
rescue ProjectWiki::CouldNotCreateWikiError
|
||||
flash[:notice] = "Could not create Wiki Repository at this time. Please try again later."
|
||||
redirect_to project_path(@project)
|
||||
|
|
|
@ -296,4 +296,13 @@ module ApplicationHelper
|
|||
def page_class
|
||||
"issue-boards-page" if current_controller?(:boards)
|
||||
end
|
||||
|
||||
# Returns active css class when condition returns true
|
||||
# otherwise returns nil.
|
||||
#
|
||||
# Example:
|
||||
# %li{ class: active_when(params[:filter] == '1') }
|
||||
def active_when(condition)
|
||||
'active' if condition
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
module BuildsHelper
|
||||
def sidebar_build_class(build, current_build)
|
||||
build_class = ''
|
||||
build_class += ' active' if build == current_build
|
||||
build_class += ' active' if build.id === current_build.id
|
||||
build_class += ' retried' if build.retried?
|
||||
build_class
|
||||
end
|
||||
|
|
|
@ -75,10 +75,10 @@ module MergeRequestsHelper
|
|||
new_namespace_project_merge_request_path(
|
||||
@project.namespace, @project,
|
||||
merge_request: {
|
||||
source_project_id: @merge_request.source_project_id,
|
||||
target_project_id: @merge_request.target_project_id,
|
||||
source_branch: @merge_request.source_branch,
|
||||
target_branch: @merge_request.target_branch,
|
||||
source_project_id: merge_request.source_project_id,
|
||||
target_project_id: merge_request.target_project_id,
|
||||
source_branch: merge_request.source_branch,
|
||||
target_branch: merge_request.target_branch,
|
||||
},
|
||||
change_branches: true
|
||||
)
|
||||
|
|
|
@ -10,7 +10,7 @@ module NamespacesHelper
|
|||
data_attr_users = { 'data-options-parent' => 'users' }
|
||||
|
||||
group_opts = [
|
||||
"Groups", groups.sort_by(&:human_name).map { |g| [display_path ? g.path : g.human_name, g.id, data_attr_group] }
|
||||
"Groups", groups.sort_by(&:human_name).map { |g| [display_path ? g.full_path : g.human_name, g.id, data_attr_group] }
|
||||
]
|
||||
|
||||
users_opts = [
|
||||
|
|
|
@ -1,10 +1,4 @@
|
|||
module NavHelper
|
||||
def page_sidebar_class
|
||||
if pinned_nav?
|
||||
"page-sidebar-expanded page-sidebar-pinned"
|
||||
end
|
||||
end
|
||||
|
||||
def page_gutter_class
|
||||
if current_path?('merge_requests#show') ||
|
||||
current_path?('merge_requests#diffs') ||
|
||||
|
@ -32,10 +26,6 @@ module NavHelper
|
|||
class_name = ''
|
||||
class_name << " with-horizontal-nav" if defined?(nav) && nav
|
||||
|
||||
if pinned_nav?
|
||||
class_name << " header-sidebar-expanded header-sidebar-pinned"
|
||||
end
|
||||
|
||||
class_name
|
||||
end
|
||||
|
||||
|
@ -46,8 +36,4 @@ module NavHelper
|
|||
def nav_control_class
|
||||
"nav-control" if current_user
|
||||
end
|
||||
|
||||
def pinned_nav?
|
||||
cookies[:pin_nav] == 'true'
|
||||
end
|
||||
end
|
||||
|
|
|
@ -34,6 +34,10 @@ module PageLayoutHelper
|
|||
end
|
||||
end
|
||||
|
||||
def favicon
|
||||
Rails.env.development? ? 'favicon-blue.ico' : 'favicon.ico'
|
||||
end
|
||||
|
||||
def page_image
|
||||
default = image_url('gitlab_logo.png')
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ module PreferencesHelper
|
|||
|
||||
if defined.size != DASHBOARD_CHOICES.size
|
||||
# Ensure that anyone adding new options updates this method too
|
||||
raise RuntimeError, "`User` defines #{defined.size} dashboard choices," +
|
||||
raise "`User` defines #{defined.size} dashboard choices," \
|
||||
" but `DASHBOARD_CHOICES` defined #{DASHBOARD_CHOICES.size}."
|
||||
else
|
||||
defined.map do |key, _|
|
||||
|
@ -41,10 +41,6 @@ module PreferencesHelper
|
|||
]
|
||||
end
|
||||
|
||||
def user_application_theme
|
||||
Gitlab::Themes.for_user(current_user).css_class
|
||||
end
|
||||
|
||||
def user_color_scheme
|
||||
Gitlab::ColorSchemes.for_user(current_user).css_class
|
||||
end
|
||||
|
|
|
@ -63,7 +63,7 @@ module SubmoduleHelper
|
|||
namespace = components.pop.gsub(/^\.\.$/, '')
|
||||
|
||||
if namespace.empty?
|
||||
namespace = @project.namespace.path
|
||||
namespace = @project.namespace.full_path
|
||||
end
|
||||
|
||||
[
|
||||
|
|
|
@ -15,6 +15,7 @@ module TodosHelper
|
|||
when Todo::MARKED then 'added a todo for'
|
||||
when Todo::APPROVAL_REQUIRED then 'set you as an approver for'
|
||||
when Todo::UNMERGEABLE then 'Could not merge'
|
||||
when Todo::DIRECTLY_ADDRESSED then 'directly addressed you on'
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -88,7 +89,8 @@ module TodosHelper
|
|||
{ id: Todo::ASSIGNED, text: 'Assigned' },
|
||||
{ id: Todo::MENTIONED, text: 'Mentioned' },
|
||||
{ id: Todo::MARKED, text: 'Added' },
|
||||
{ id: Todo::BUILD_FAILED, text: 'Pipelines' }
|
||||
{ id: Todo::BUILD_FAILED, text: 'Pipelines' },
|
||||
{ id: Todo::DIRECTLY_ADDRESSED, text: 'Directly addressed' }
|
||||
]
|
||||
end
|
||||
|
||||
|
|
13
app/helpers/wiki_helper.rb
Normal file
13
app/helpers/wiki_helper.rb
Normal file
|
@ -0,0 +1,13 @@
|
|||
module WikiHelper
|
||||
# Produces a pure text breadcrumb for a given page.
|
||||
#
|
||||
# page_slug - The slug of a WikiPage object.
|
||||
#
|
||||
# Returns a String composed of the capitalized name of each directory and the
|
||||
# capitalized name of the page itself.
|
||||
def breadcrumb(page_slug)
|
||||
page_slug.split('/').
|
||||
map { |dir_or_page| WikiPage.unhyphenize(dir_or_page).capitalize }.
|
||||
join(' / ')
|
||||
end
|
||||
end
|
|
@ -151,7 +151,7 @@ class Notify < BaseMailer
|
|||
headers['In-Reply-To'] = message_id(model)
|
||||
headers['References'] = message_id(model)
|
||||
|
||||
headers[:subject].prepend('Re: ') if headers[:subject]
|
||||
headers[:subject]&.prepend('Re: ')
|
||||
|
||||
mail_thread(model, headers)
|
||||
end
|
||||
|
|
|
@ -116,31 +116,25 @@ class ApplicationSetting < ActiveRecord::Base
|
|||
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
|
||||
|
||||
validates_each :restricted_visibility_levels do |record, attr, value|
|
||||
unless value.nil?
|
||||
value.each do |level|
|
||||
unless Gitlab::VisibilityLevel.options.has_value?(level)
|
||||
record.errors.add(attr, "'#{level}' is not a valid visibility level")
|
||||
end
|
||||
value&.each do |level|
|
||||
unless Gitlab::VisibilityLevel.options.has_value?(level)
|
||||
record.errors.add(attr, "'#{level}' is not a valid visibility level")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
validates_each :import_sources do |record, attr, value|
|
||||
unless value.nil?
|
||||
value.each do |source|
|
||||
unless Gitlab::ImportSources.options.has_value?(source)
|
||||
record.errors.add(attr, "'#{source}' is not a import source")
|
||||
end
|
||||
value&.each do |source|
|
||||
unless Gitlab::ImportSources.options.has_value?(source)
|
||||
record.errors.add(attr, "'#{source}' is not a import source")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
validates_each :disabled_oauth_sign_in_sources do |record, attr, value|
|
||||
unless value.nil?
|
||||
value.each do |source|
|
||||
unless Devise.omniauth_providers.include?(source.to_sym)
|
||||
record.errors.add(attr, "'#{source}' is not an OAuth sign-in source")
|
||||
end
|
||||
value&.each do |source|
|
||||
unless Devise.omniauth_providers.include?(source.to_sym)
|
||||
record.errors.add(attr, "'#{source}' is not an OAuth sign-in source")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -230,11 +224,11 @@ class ApplicationSetting < ActiveRecord::Base
|
|||
end
|
||||
|
||||
def domain_whitelist_raw
|
||||
self.domain_whitelist.join("\n") unless self.domain_whitelist.nil?
|
||||
self.domain_whitelist&.join("\n")
|
||||
end
|
||||
|
||||
def domain_blacklist_raw
|
||||
self.domain_blacklist.join("\n") unless self.domain_blacklist.nil?
|
||||
self.domain_blacklist&.join("\n")
|
||||
end
|
||||
|
||||
def domain_whitelist_raw=(values)
|
||||
|
|
|
@ -16,6 +16,14 @@ class AwardEmoji < ActiveRecord::Base
|
|||
scope :downvotes, -> { where(name: DOWNVOTE_NAME) }
|
||||
scope :upvotes, -> { where(name: UPVOTE_NAME) }
|
||||
|
||||
class << self
|
||||
def votes_for_collection(ids, type)
|
||||
select('name', 'awardable_id', 'COUNT(*) as count').
|
||||
where('name IN (?) AND awardable_type = ? AND awardable_id IN (?)', [DOWNVOTE_NAME, UPVOTE_NAME], type, ids).
|
||||
group('name', 'awardable_id')
|
||||
end
|
||||
end
|
||||
|
||||
def downvote?
|
||||
self.name == DOWNVOTE_NAME
|
||||
end
|
||||
|
|
|
@ -22,8 +22,6 @@ module Ci
|
|||
scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) }
|
||||
scope :ordered, ->() { order(id: :desc) }
|
||||
|
||||
after_save :tick_runner_queue, if: :form_editable_changed?
|
||||
|
||||
scope :owned_or_shared, ->(project_id) do
|
||||
joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id')
|
||||
.where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
|
||||
|
@ -40,6 +38,8 @@ module Ci
|
|||
|
||||
acts_as_taggable
|
||||
|
||||
after_destroy :cleanup_runner_queue
|
||||
|
||||
# Searches for runners matching the given query.
|
||||
#
|
||||
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
|
||||
|
@ -147,14 +147,14 @@ module Ci
|
|||
|
||||
private
|
||||
|
||||
def runner_queue_key
|
||||
"runner:build_queue:#{self.token}"
|
||||
def cleanup_runner_queue
|
||||
Gitlab::Redis.with do |redis|
|
||||
redis.del(runner_queue_key)
|
||||
end
|
||||
end
|
||||
|
||||
def form_editable_changed?
|
||||
FORM_EDITABLE.any? do |editable|
|
||||
public_send("#{editable}_changed?")
|
||||
end
|
||||
def runner_queue_key
|
||||
"runner:build_queue:#{self.token}"
|
||||
end
|
||||
|
||||
def tag_constraints
|
||||
|
|
|
@ -15,6 +15,11 @@ module Issuable
|
|||
include Taskable
|
||||
include TimeTrackable
|
||||
|
||||
# This object is used to gather issuable meta data for displaying
|
||||
# upvotes, downvotes and notes count for issues and merge requests
|
||||
# lists avoiding n+1 queries and improving performance.
|
||||
IssuableMeta = Struct.new(:upvotes, :downvotes, :notes_count)
|
||||
|
||||
included do
|
||||
cache_markdown_field :title, pipeline: :single_line
|
||||
cache_markdown_field :description
|
||||
|
@ -95,8 +100,8 @@ module Issuable
|
|||
def update_assignee_cache_counts
|
||||
# make sure we flush the cache for both the old *and* new assignees(if they exist)
|
||||
previous_assignee = User.find_by_id(assignee_id_was) if assignee_id_was
|
||||
previous_assignee.update_cache_counts if previous_assignee
|
||||
assignee.update_cache_counts if assignee
|
||||
previous_assignee&.update_cache_counts
|
||||
assignee&.update_cache_counts
|
||||
end
|
||||
|
||||
# We want to use optimistic lock for cases when only title or description are involved
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue