Commit 35ca13e1 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into 3776-ci-view-for-sast

* master: (123 commits)
  Add changelog entry
  Add tests for license expiration duration message
  Show license type in expiration only when applied license type is trial
  Replace "Kubernetes cluster" where appropriate in docs
  remove pipelines_charts.js
  Improve SAST message
  deleted pipelines_charts file
  Ignore `_links` property for no-underscore-dangle rule
  Improve error handling for Gitlab::Profiler and improve doc about providing a user
  Add axios to frontend docs as the default network request library
  Remove Webpack bundle tag for users:show
  Replace $.ajax with axios in members.js
  Remove duplicate imports
  Replace $.get with axios in importerStatus
  Refactor ee dispatcher groups:*
  Add scripts/ee-files-location-check to check wrong location of new EE-specific files
  Filter secret variable values from logs
  Resolve "Change icon and color of issue statuses in related issues and epic issues"
  Re-enable eslint in `commits.js` file
  Fixes Renaming repository (project[name]) fails on 2nd try whend the validation has failed
  ...
parents 0816d293 9090d0c2
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
"import/no-commonjs": "error", "import/no-commonjs": "error",
"no-multiple-empty-lines": ["error", { "max": 1 }], "no-multiple-empty-lines": ["error", { "max": 1 }],
"promise/catch-or-return": "error", "promise/catch-or-return": "error",
"no-underscore-dangle": ["error", { "allow": ["__"]}], "no-underscore-dangle": ["error", { "allow": ["__", "_links"]}],
"vue/html-self-closing": ["error", { "vue/html-self-closing": ["error", {
"html": { "html": {
"void": "always", "void": "always",
......
...@@ -289,6 +289,14 @@ retrieve-tests-metadata: ...@@ -289,6 +289,14 @@ retrieve-tests-metadata:
- wget -O $EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH || rm $EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH - wget -O $EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH http://${TESTS_METADATA_S3_BUCKET}.s3.amazonaws.com/$EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH || rm $EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH
- '[[ -f $EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH}' - '[[ -f $EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH ]] || echo "{}" > ${EE_KNAPSACK_RSPEC_SUITE_REPORT_PATH}'
ee-files-location-check:
<<: *dedicated-runner
stage: prepare
before_script: []
cache: {}
script:
- scripts/ee-files-location-check
update-tests-metadata: update-tests-metadata:
<<: *tests-metadata-state <<: *tests-metadata-state
<<: *only-canonical-masters <<: *only-canonical-masters
......
...@@ -175,7 +175,7 @@ Assigning a team label makes sure issues get the attention of the appropriate ...@@ -175,7 +175,7 @@ Assigning a team label makes sure issues get the attention of the appropriate
people. people.
The current team labels are ~Build, ~"CI/CD", ~Discussion, ~Documentation, ~Edge, The current team labels are ~Build, ~"CI/CD", ~Discussion, ~Documentation, ~Edge,
~Geo, ~Gitaly, ~Platform, ~Monitoring, ~Release, and ~"UX". ~Geo, ~Gitaly, ~Monitoring, ~Platform, ~Release, ~"Security Products" and ~"UX".
The descriptions on the [labels page][labels-page] explain what falls under the The descriptions on the [labels page][labels-page] explain what falls under the
responsibility of each team. responsibility of each team.
......
...@@ -71,11 +71,15 @@ star, smile, etc.). Some good tips about code reviews can be found in our ...@@ -71,11 +71,15 @@ star, smile, etc.). Some good tips about code reviews can be found in our
## Feature freeze on the 7th for the release on the 22nd ## Feature freeze on the 7th for the release on the 22nd
After the 7th (Pacific Standard Time Zone) of each month, RC1 of the upcoming release (to be shipped on the 22nd) is created and deployed to GitLab.com and the stable branch for this release is frozen, which means master is no longer merged into it. After 7th at 23:59 (Pacific Standard Time Zone) of each month, RC1 of the upcoming release (to be shipped on the 22nd) is created and deployed to GitLab.com and the stable branch for this release is frozen, which means master is no longer merged into it.
Merge requests may still be merged into master during this period, Merge requests may still be merged into master during this period,
but they will go into the _next_ release, unless they are manually cherry-picked into the stable branch. but they will go into the _next_ release, unless they are manually cherry-picked into the stable branch.
By freezing the stable branches 2 weeks prior to a release, we reduce the risk of a last minute merge request potentially breaking things. By freezing the stable branches 2 weeks prior to a release, we reduce the risk of a last minute merge request potentially breaking things.
Any release candidate that gets created after this date can become a final release,
hence the name release candidate.
### Between the 1st and the 7th ### Between the 1st and the 7th
These types of merge requests for the upcoming release need special consideration: These types of merge requests for the upcoming release need special consideration:
...@@ -193,11 +197,10 @@ to be backported down to the `9.5` release, you will need to assign it the ...@@ -193,11 +197,10 @@ to be backported down to the `9.5` release, you will need to assign it the
### Asking for an exception ### Asking for an exception
If you think a merge request should go into an RC or patch even though it does not meet these requirements, If you think a merge request should go into an RC or patch even though it does not meet these requirements,
you can ask for an exception to be made. Exceptions require sign-off from 3 people besides the developer: you can ask for an exception to be made.
1. a Release Manager Go to [Release tasks issue tracker](https://gitlab.com/gitlab-org/release/tasks/issues/new) and create an issue
2. an Engineering Lead using the `Exception-request` issue template.
3. an Engineering Director, the VP of Engineering, or the CTO
You can find who is who on the [team page](https://about.gitlab.com/team/). You can find who is who on the [team page](https://about.gitlab.com/team/).
......
...@@ -50,10 +50,8 @@ class AwardsHandler { ...@@ -50,10 +50,8 @@ class AwardsHandler {
this.registerEventListener('on', $('html'), 'click', (e) => { this.registerEventListener('on', $('html'), 'click', (e) => {
const $target = $(e.target); const $target = $(e.target);
if (!$target.closest('.emoji-menu-content').length) {
$('.js-awards-block.current').removeClass('current');
}
if (!$target.closest('.emoji-menu').length) { if (!$target.closest('.emoji-menu').length) {
$('.js-awards-block.current').removeClass('current');
if ($('.emoji-menu').is(':visible')) { if ($('.emoji-menu').is(':visible')) {
$('.js-add-award.is-active').removeClass('is-active'); $('.js-add-award.is-active').removeClass('is-active');
this.hideMenuElement($('.emoji-menu')); this.hideMenuElement($('.emoji-menu'));
......
...@@ -75,6 +75,7 @@ export default class AjaxVariableList { ...@@ -75,6 +75,7 @@ export default class AjaxVariableList {
if (res.status === statusCodes.OK && res.data) { if (res.status === statusCodes.OK && res.data) {
this.updateRowsWithPersistedVariables(res.data.variables); this.updateRowsWithPersistedVariables(res.data.variables);
this.variableList.hideValues();
} else if (res.status === statusCodes.BAD_REQUEST) { } else if (res.status === statusCodes.BAD_REQUEST) {
// Validation failed // Validation failed
this.errorBox.innerHTML = generateErrorBoxContent(res.data); this.errorBox.innerHTML = generateErrorBoxContent(res.data);
......
...@@ -178,6 +178,10 @@ export default class VariableList { ...@@ -178,6 +178,10 @@ export default class VariableList {
this.$container.find('.js-row-remove-button').attr('disabled', !isEnabled); this.$container.find('.js-row-remove-button').attr('disabled', !isEnabled);
} }
hideValues() {
this.secretValues.updateDom(false);
}
getAllData() { getAllData() {
// Ignore the last empty row because we don't want to try persist // Ignore the last empty row because we don't want to try persist
// a blank variable and run into validation problems. // a blank variable and run into validation problems.
......
/* eslint-disable func-names, wrap-iife, consistent-return,
no-return-assign, no-param-reassign, one-var-declaration-per-line, no-unused-vars,
prefer-template, object-shorthand, prefer-arrow-callback */
import { pluralize } from './lib/utils/text_utility'; import { pluralize } from './lib/utils/text_utility';
import { localTimeAgo } from './lib/utils/datetime_utility'; import { localTimeAgo } from './lib/utils/datetime_utility';
import Pager from './pager'; import Pager from './pager';
import axios from './lib/utils/axios_utils'; import axios from './lib/utils/axios_utils';
export default (function () { export default class CommitsList {
const CommitsList = {}; constructor(limit = 0) {
this.timer = null;
CommitsList.timer = null;
CommitsList.init = function (limit) {
this.$contentList = $('.content_list'); this.$contentList = $('.content_list');
$('body').on('click', '.day-commits-table li.commit', function (e) { Pager.init(parseInt(limit, 10), false, false, this.processCommits.bind(this));
if (e.target.nodeName !== 'A') {
location.href = $(this).attr('url');
e.stopPropagation();
return false;
}
});
Pager.init(parseInt(limit, 10), false, false, this.processCommits);
this.content = $('#commits-list'); this.content = $('#commits-list');
this.searchField = $('#commits-search'); this.searchField = $('#commits-search');
this.lastSearch = this.searchField.val(); this.lastSearch = this.searchField.val();
return this.initSearch(); this.initSearch();
}; }
CommitsList.initSearch = function () { initSearch() {
this.timer = null; this.timer = null;
return this.searchField.keyup((function (_this) { this.searchField.on('keyup', () => {
return function () { clearTimeout(this.timer);
clearTimeout(_this.timer); this.timer = setTimeout(this.filterResults.bind(this), 500);
return _this.timer = setTimeout(_this.filterResults, 500); });
}; }
})(this));
};
CommitsList.filterResults = function () { filterResults() {
const form = $('.commits-search-form'); const form = $('.commits-search-form');
const search = CommitsList.searchField.val(); const search = this.searchField.val();
if (search === CommitsList.lastSearch) return Promise.resolve(); if (search === this.lastSearch) return Promise.resolve();
const commitsUrl = form.attr('action') + '?' + form.serialize(); const commitsUrl = `${form.attr('action')}?${form.serialize()}`;
CommitsList.content.fadeTo('fast', 0.5); this.content.fadeTo('fast', 0.5);
const params = form.serializeArray().reduce((acc, obj) => Object.assign(acc, { const params = form.serializeArray().reduce((acc, obj) => Object.assign(acc, {
[obj.name]: obj.value, [obj.name]: obj.value,
}), {}); }), {});
...@@ -55,9 +39,9 @@ export default (function () { ...@@ -55,9 +39,9 @@ export default (function () {
params, params,
}) })
.then(({ data }) => { .then(({ data }) => {
CommitsList.lastSearch = search; this.lastSearch = search;
CommitsList.content.html(data.html); this.content.html(data.html);
CommitsList.content.fadeTo('fast', 1.0); this.content.fadeTo('fast', 1.0);
// Change url so if user reload a page - search results are saved // Change url so if user reload a page - search results are saved
history.replaceState({ history.replaceState({
...@@ -65,16 +49,16 @@ export default (function () { ...@@ -65,16 +49,16 @@ export default (function () {
}, document.title, commitsUrl); }, document.title, commitsUrl);
}) })
.catch(() => { .catch(() => {
CommitsList.content.fadeTo('fast', 1.0); this.content.fadeTo('fast', 1.0);
CommitsList.lastSearch = null; this.lastSearch = null;
}); });
}; }
// Prepare loaded data. // Prepare loaded data.
CommitsList.processCommits = (data) => { processCommits(data) {
let processedData = data; let processedData = data;
const $processedData = $(processedData); const $processedData = $(processedData);
const $commitsHeadersLast = CommitsList.$contentList.find('li.js-commit-header').last(); const $commitsHeadersLast = this.$contentList.find('li.js-commit-header').last();
const lastShownDay = $commitsHeadersLast.data('day'); const lastShownDay = $commitsHeadersLast.data('day');
const $loadedCommitsHeadersFirst = $processedData.filter('li.js-commit-header').first(); const $loadedCommitsHeadersFirst = $processedData.filter('li.js-commit-header').first();
const loadedShownDayFirst = $loadedCommitsHeadersFirst.data('day'); const loadedShownDayFirst = $loadedCommitsHeadersFirst.data('day');
...@@ -97,7 +81,5 @@ export default (function () { ...@@ -97,7 +81,5 @@ export default (function () {
localTimeAgo($processedData.find('.js-timeago')); localTimeAgo($processedData.find('.js-timeago'));
return processedData; return processedData;
}; }
}
return CommitsList;
})();
...@@ -11,10 +11,8 @@ import UsersSelect from './users_select'; ...@@ -11,10 +11,8 @@ import UsersSelect from './users_select';
import UserCallout from './user_callout'; import UserCallout from './user_callout';
import ZenMode from './zen_mode'; import ZenMode from './zen_mode';
import initGeoInfoModal from 'ee/init_geo_info_modal'; // eslint-disable-line import/first import initGeoInfoModal from 'ee/init_geo_info_modal'; // eslint-disable-line import/first
import initGroupAnalytics from 'ee/init_group_analytics'; // eslint-disable-line import/first
import initPathLocks from 'ee/path_locks'; // eslint-disable-line import/first import initPathLocks from 'ee/path_locks'; // eslint-disable-line import/first
import initApprovals from 'ee/approvals'; // eslint-disable-line import/first import initApprovals from 'ee/approvals'; // eslint-disable-line import/first
import initLDAPGroupsSelect from 'ee/ldap_groups_select'; // eslint-disable-line import/first
var Dispatcher; var Dispatcher;
...@@ -234,6 +232,11 @@ var Dispatcher; ...@@ -234,6 +232,11 @@ var Dispatcher;
.then(callDefault) .then(callDefault)
.catch(fail); .catch(fail);
break; break;
case 'projects:services:edit':
import('./pages/projects/services/edit')
.then(callDefault)
.catch(fail);
break;
case 'projects:snippets:edit': case 'projects:snippets:edit':
case 'projects:snippets:update': case 'projects:snippets:update':
import('./pages/projects/snippets/edit') import('./pages/projects/snippets/edit')
...@@ -473,11 +476,6 @@ var Dispatcher; ...@@ -473,11 +476,6 @@ var Dispatcher;
.then(callDefault) .then(callDefault)
.catch(fail); .catch(fail);
break; break;
case 'users:show':
import('./pages/users/show')
.then(callDefault)
.catch(fail);
break;
case 'admin:conversational_development_index:show': case 'admin:conversational_development_index:show':
import('./pages/admin/conversational_development_index/show') import('./pages/admin/conversational_development_index/show')
.then(callDefault) .then(callDefault)
...@@ -677,10 +675,14 @@ var Dispatcher; ...@@ -677,10 +675,14 @@ var Dispatcher;
import(/* webpackChunkName: "admin_licenses" */ 'ee/pages/admin/licenses/new').then(m => m.default()).catch(fail); import(/* webpackChunkName: "admin_licenses" */ 'ee/pages/admin/licenses/new').then(m => m.default()).catch(fail);
break; break;
case 'groups:analytics:show': case 'groups:analytics:show':
initGroupAnalytics(); import(/* webpackChunkName: "ee_groups_analytics_show" */ 'ee/pages/groups/analytics/show')
.then(callDefault)
.catch(fail);
break; break;
case 'groups:ldap_group_links:index': case 'groups:ldap_group_links:index':
initLDAPGroupsSelect(); import(/* webpackChunkName: "ee_groups_ldap_links" */ 'ee/pages/groups/ldap_group_links')
.then(callDefault)
.catch(fail);
break; break;
case 'admin:groups:edit': case 'admin:groups:edit':
import(/* webpackChunkName: "ee_admin_groups_edit" */ 'ee/pages/admin/groups/edit').then(m => m.default()).catch(fail); import(/* webpackChunkName: "ee_admin_groups_edit" */ 'ee/pages/admin/groups/edit').then(m => m.default()).catch(fail);
......
...@@ -59,29 +59,36 @@ class ImporterStatus { ...@@ -59,29 +59,36 @@ class ImporterStatus {
.catch(() => flash(__('An error occurred while importing project'))); .catch(() => flash(__('An error occurred while importing project')));
} }
setAutoUpdate() { autoUpdate() {
return setInterval(() => $.get(this.jobsUrl, data => $.each(data, (i, job) => { return axios.get(this.jobsUrl)
const jobItem = $(`#project_${job.id}`); .then(({ data = [] }) => {
const statusField = jobItem.find('.job-status'); data.forEach((job) => {
const jobItem = $(`#project_${job.id}`);
const statusField = jobItem.find('.job-status');
const spinner = '<i class="fa fa-spinner fa-spin"></i>';
const spinner = '<i class="fa fa-spinner fa-spin"></i>'; switch (job.import_status) {
case 'finished':
jobItem.removeClass('active').addClass('success');
statusField.html('<span><i class="fa fa-check"></i> done</span>');
break;
case 'scheduled':
statusField.html(`${spinner} scheduled`);
break;
case 'started':
statusField.html(`${spinner} started`);
break;
default:
statusField.html(job.import_status);
break;
}
});
});
}
switch (job.import_status) { setAutoUpdate() {
case 'finished': setInterval(this.autoUpdate.bind(this), 4000);
jobItem.removeClass('active').addClass('success');
statusField.html('<span><i class="fa fa-check"></i> done</span>');
break;
case 'scheduled':
statusField.html(`${spinner} scheduled`);
break;
case 'started':
statusField.html(`${spinner} started`);
break;
default:
statusField.html(job.import_status);
break;
}
})), 4000);
} }
} }
......
...@@ -7,8 +7,6 @@ import Sidebar from './right_sidebar'; ...@@ -7,8 +7,6 @@ import Sidebar from './right_sidebar';
import DueDateSelectors from './due_date_select'; import DueDateSelectors from './due_date_select';
import WeightSelect from 'ee/weight_select'; // eslint-disable-line import/first
export default () => { export default () => {
const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML); const sidebarOptions = JSON.parse(document.querySelector('.js-sidebar-options').innerHTML);
...@@ -16,7 +14,6 @@ export default () => { ...@@ -16,7 +14,6 @@ export default () => {
full_path: sidebarOptions.fullPath, full_path: sidebarOptions.fullPath,
}); });
new LabelsSelect(); new LabelsSelect();
new WeightSelect();
new IssuableContext(sidebarOptions.currentUser); new IssuableContext(sidebarOptions.currentUser);
new DueDateSelectors(); new DueDateSelectors();
Sidebar.initialize(); Sidebar.initialize();
......
...@@ -63,7 +63,7 @@ const mixins = { ...@@ -63,7 +63,7 @@ const mixins = {
return this.title.length > 0; return this.title.length > 0;
}, },
iconName() { iconName() {
return this.isOpen ? 'issue-open-m' : 'cut'; return this.isOpen ? 'issue-open' : 'issue-close';
}, },
iconClass() { iconClass() {
return this.isOpen ? 'issue-token-state-icon-open' : 'issue-token-state-icon-closed'; return this.isOpen ? 'issue-token-state-icon-open' : 'issue-token-state-icon-closed';
......
import Flash from './flash'; import Flash from './flash';
import { __ } from './locale';
import axios from './lib/utils/axios_utils';
export default class Members { export default class Members {
constructor() { constructor() {
...@@ -50,10 +52,11 @@ export default class Members { ...@@ -50,10 +52,11 @@ export default class Members {
$toggle.disable(); $toggle.disable();
$dateInput.disable(); $dateInput.disable();
this.overrideLdap($memberListItem, $link.data('endpoint'), false).fail(() => { this.overrideLdap($memberListItem, $link.data('endpoint'), false)
$toggle.enable(); .catch(() => {
$dateInput.enable(); $toggle.enable();
}); $dateInput.enable();
});
} }
}, },
}); });
...@@ -101,32 +104,31 @@ export default class Members { ...@@ -101,32 +104,31 @@ export default class Members {
$btn.disable(); $btn.disable();
// eslint-disable-next-line promise/catch-or-return // eslint-disable-next-line promise/catch-or-return
this.overrideLdap($memberListItem, $btn.data('endpoint'), true).then(() => { this.overrideLdap($memberListItem, $btn.data('endpoint'), true)
this.showLDAPPermissionsWarning(e); .then(() => {
this.showLDAPPermissionsWarning(e);
$toggle.enable();
$dateInput.enable(); $toggle.enable();
}).fail((xhr) => { $dateInput.enable();
$btn.enable(); })
.catch((xhr) => {
if (xhr.status === 403) { $btn.enable();
Flash('You do not have the correct permissions to override the settings from the LDAP group sync.', 'alert');
} else { if (xhr.status === 403) {
Flash('An error occured whilst saving LDAP override status. Please try again.', 'alert'); Flash(__('You do not have the correct permissions to override the settings from the LDAP group sync.'));
} } else {
}); Flash(__('An error occurred while saving LDAP override status. Please try again.'));
}
});
} }
// eslint-disable-next-line class-methods-use-this // eslint-disable-next-line class-methods-use-this
overrideLdap($memberListitem, endpoint, override) { overrideLdap($memberListitem, endpoint, override) {
return $.ajax({ return axios.patch(endpoint, {
url: endpoint, group_member: {
type: 'PATCH', override,
data: {
group_member: {
override,
},
}, },
}).then(() => { })
.then(() => {
$memberListitem.toggleClass('is-overriden', override); $memberListitem.toggleClass('is-overriden', override);
}); });
} }
......
...@@ -69,20 +69,20 @@ export default class MirrorPull { ...@@ -69,20 +69,20 @@ export default class MirrorPull {
// Make backOff polling to get data // Make backOff polling to get data
backOff((next, stop) => { backOff((next, stop) => {
$.getJSON(`${projectMirrorSSHEndpoint}?ssh_url=${repositoryUrl}`) axios.get(`${projectMirrorSSHEndpoint}?ssh_url=${repositoryUrl}`)
.done((res, statusText, header) => { .then(({ data, status }) => {
if (header.status === 204) { if (status === 204) {
this.backOffRequestCounter = this.backOffRequestCounter += 1; this.backOffRequestCounter = this.backOffRequestCounter += 1;
if (this.backOffRequestCounter < 3) { if (this.backOffRequestCounter < 3) {
next(); next();
} else {
stop(res);
}
} else { } else {
stop(res); stop(data);
} }
}) } else {
.fail(stop); stop(data);
}
})
.catch(stop);
}) })
.then((res) => { .then((res) => {
$btnLoadSpinner.addClass('hidden'); $btnLoadSpinner.addClass('hidden');
...@@ -92,10 +92,11 @@ export default class MirrorPull { ...@@ -92,10 +92,11 @@ export default class MirrorPull {
this.showSSHInformation(res); this.showSSHInformation(res);
} }
}) })
.catch((res) => { .catch(({ response }) => {
// Show failure message when there's an error and re-enable Detect host keys button // Show failure message when there's an error and re-enable Detect host keys button
const failureMessage = res.responseJSON ? res.responseJSON.message : 'Something went wrong on our end.'; const failureMessage = response.data ? response.data.message : __('An error occurred while detecting host keys');
Flash(failureMessage); // eslint-disable-line Flash(failureMessage); // eslint-disable-line
$btnLoadSpinner.addClass('hidden'); $btnLoadSpinner.addClass('hidden');
this.$btnDetectHostKeys.enable(); this.$btnDetectHostKeys.enable();
}); });
......
<script> <script>
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import Flash from '~/flash'; import createFlash from '~/flash';
import modal from '~/vue_shared/components/modal.vue'; import GlModal from '~/vue_shared/components/gl_modal.vue';
import { s__ } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility'; import { redirectTo } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
export default { export default {
components: { components: {
modal, GlModal,
}, },
props: { props: {
url: { url: {
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
}, },
computed: { computed: {
text() { text() {
return s__('AdminArea|You’re about to stop all jobs. This will halt all current jobs that are running.'); return s__('AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running.');
}, },
}, },
methods: { methods: {
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
redirectTo(response.request.responseURL); redirectTo(response.request.responseURL);
}) })
.catch((error) => { .catch((error) => {
Flash(s__('AdminArea|Stopping jobs failed')); createFlash(s__('AdminArea|Stopping jobs failed'));
throw error; throw error;
}); });
}, },
...@@ -37,11 +37,13 @@ ...@@ -37,11 +37,13 @@
</script> </script>
<template> <template>
<modal <gl-modal
id="stop-jobs-modal" id="stop-jobs-modal"
:title="s__('AdminArea|Stop all jobs?')" :header-title-text="s__('AdminArea|Stop all jobs?')"
:text="text" footer-primary-button-variant="danger"
kind="danger" :footer-primary-button-text="s__('AdminArea|Stop jobs')"
:primary-button-label="s__('AdminArea|Stop jobs')" @submit="onSubmit"
@submit="onSubmit" /> >
{{ text }}
</gl-modal>
</template> </template>
...@@ -8,22 +8,23 @@ Vue.use(Translate); ...@@ -8,22 +8,23 @@ Vue.use(Translate);
export default () => { export default () => {
const stopJobsButton = document.getElementById('stop-jobs-button'); const stopJobsButton = document.getElementById('stop-jobs-button');
if (stopJobsButton) {
// eslint-disable-next-line no-new // eslint-disable-next-line no-new
new Vue({ new Vue({
el: '#stop-jobs-modal', el: '#stop-jobs-modal',
components: { components: {
stopJobsModal, stopJobsModal,
}, },
mounted() { mounted() {
stopJobsButton.classList.remove('disabled'); stopJobsButton.classList.remove('disabled');
}, },
render(createElement) { render(createElement) {
return createElement('stop-jobs-modal', { return createElement('stop-jobs-modal', {
props: { props: {
url: stopJobsButton.dataset.url, url: stopJobsButton.dataset.url,
}, },
}); });
}, },
}); });
}
}; };
...@@ -3,7 +3,7 @@ import GpgBadges from '~/gpg_badges'; ...@@ -3,7 +3,7 @@ import GpgBadges from '~/gpg_badges';
import ShortcutsNavigation from '~/shortcuts_navigation'; import ShortcutsNavigation from '~/shortcuts_navigation';
export default () => { export default () => {
CommitsList.init(document.querySelector('.js-project-commits-show').dataset.commitsLimit); new CommitsList(document.querySelector('.js-project-commits-show').dataset.commitsLimit); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new
GpgBadges.fetch(); GpgBadges.fetch();
}; };
import Chart from 'vendor/Chart';
const options = {
scaleOverlay: true,
responsive: true,
maintainAspectRatio: false,
};
const buildChart = (chartScope) => {
const data = {
labels: chartScope.labels,
datasets: [{
fillColor: '#707070',
strokeColor: '#707070',
pointColor: '#707070',
pointStrokeColor: '#EEE',
data: chartScope.totalValues,
},
{
fillColor: '#1aaa55',
strokeColor: '#1aaa55',
pointColor: '#1aaa55',
pointStrokeColor: '#fff',
data: chartScope.successValues,
},
],
};
const ctx = $(`#${chartScope.scope}Chart`).get(0).getContext('2d');
new Chart(ctx).Line(data, options);
};
document.addEventListener('DOMContentLoaded', () => {
const chartTimesData = JSON.parse(document.getElementById('pipelinesTimesChartsData').innerHTML);
const chartsData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML);
const data = {
labels: chartTimesData.labels,
datasets: [{
fillColor: 'rgba(220,220,220,0.5)',
strokeColor: 'rgba(220,220,220,1)',
barStrokeWidth: 1,
barValueSpacing: 1,
barDatasetSpacing: 1,
data: chartTimesData.values,
}],
};
if (window.innerWidth < 768) {
// Scale fonts if window width lower than 768px (iPad portrait)
options.scaleFontSize = 8;
}
new Chart($('#build_timesChart').get(0).getContext('2d')).Bar(data, options);
chartsData.forEach(scope => buildChart(scope));
});
/* eslint-disable no-new */ import IntegrationSettingsForm from '~/integrations/integration_settings_form';
import IntegrationSettingsForm from './integration_settings_form';
$(() => { export default () => {
const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form'); const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
integrationSettingsForm.init(); integrationSettingsForm.init();
}); };
import UserCallout from '~/user_callout';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import UserTabs from './user_tabs'; import UserTabs from './user_tabs';
...@@ -22,4 +23,5 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -22,4 +23,5 @@ document.addEventListener('DOMContentLoaded', () => {
const page = $('body').attr('data-page'); const page = $('body').attr('data-page');
const action = page.split(':')[1]; const action = page.split(':')[1];
initUserProfile(action); initUserProfile(action);
new UserCallout(); // eslint-disable-line no-new
}); });
import UserCallout from '~/user_callout';
export default () => new UserCallout();
import axios from '../lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import Activities from '../activities'; import Activities from '~/activities';
import { localTimeAgo } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
import flash from '~/flash';
import ActivityCalendar from './activity_calendar'; import ActivityCalendar from './activity_calendar';
import { localTimeAgo } from '../lib/utils/datetime_utility';
import { __ } from '../locale';
import flash from '../flash';
/** /**
* UserTabs * UserTabs
......
import Chart from 'vendor/Chart';
document.addEventListener('DOMContentLoaded', () => {
const chartData = JSON.parse(document.getElementById('pipelinesChartsData').innerHTML);
const buildChart = (chartScope) => {
const data = {
labels: chartScope.labels,
datasets: [{
fillColor: '#707070',
strokeColor: '#707070',
pointColor: '#707070',
pointStrokeColor: '#EEE',
data: chartScope.totalValues,
},
{
fillColor: '#1aaa55',
strokeColor: '#1aaa55',
pointColor: '#1aaa55',
pointStrokeColor: '#fff',
data: chartScope.successValues,
},
],
};
const ctx = $(`#${chartScope.scope}Chart`).get(0).getContext('2d');
const options = {
scaleOverlay: true,
responsive: true,
maintainAspectRatio: false,
};
if (window.innerWidth < 768) {
// Scale fonts if window width lower than 768px (iPad portrait)
options.scaleFontSize = 8;
}
new Chart(ctx).Line(data, options);
};
chartData.forEach(scope => buildChart(scope));
});
import Chart from 'vendor/Chart';
document.addEventListener('DOMContentLoaded', () => {
const chartData = JSON.parse(document.getElementById('pipelinesTimesChartsData').innerHTML);
const data = {
labels: chartData.labels,
datasets: [{
fillColor: 'rgba(220,220,220,0.5)',
strokeColor: 'rgba(220,220,220,1)',
barStrokeWidth: 1,
barValueSpacing: 1,
barDatasetSpacing: 1,
data: chartData.values,
}],
};
const ctx = $('#build_timesChart').get(0).getContext('2d');
const options = {
scaleOverlay: true,
responsive: true,
maintainAspectRatio: false,
};
if (window.innerWidth < 768) {
// Scale fonts if window width lower than 768px (iPad portrait)
options.scaleFontSize = 8;
}
new Chart(ctx).Bar(data, options);
});
...@@ -149,6 +149,7 @@ export default { ...@@ -149,6 +149,7 @@ export default {
}, },
handleNotification(data) { handleNotification(data) {
if (data.ci_status === this.mr.ciStatus) return; if (data.ci_status === this.mr.ciStatus) return;
if (!data.pipeline) return;
const label = data.pipeline.details.status.label; const label = data.pipeline.details.status.label;
const title = `Pipeline ${label}`; const title = `Pipeline ${label}`;
......
<script>
const buttonVariants = [
'danger',
'primary',
'success',
'warning',
];
export default {
name: 'GlModal',
props: {
id: {
type: String,
required: false,
default: null,
},
headerTitleText: {
type: String,
required: false,
default: '',
},
footerPrimaryButtonVariant: {
type: String,
required: false,
default: 'primary',
validator: value => buttonVariants.indexOf(value) !== -1,
},
footerPrimaryButtonText: {
type: String,
required: false,
default: '',
},
},
methods: {
emitCancel(event) {
this.$emit('cancel', event);
},
emitSubmit(event) {
this.$emit('submit', event);
},
},
};
</script>
<template>
<div
:id="id"
class="modal fade"
tabindex="-1"
role="dialog"
>
<div
class="modal-dialog"
role="document"
>
<div class="modal-content">
<div class="modal-header">
<slot name="header">
<button
type="button"
class="close"
data-dismiss="modal"
:aria-label="s__('Modal|Close')"
@click="emitCancel($event)"
>
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">
<slot name="title">
{{ headerTitleText }}
</slot>
</h4>
</slot>
</div>
<div class="modal-body">
<slot></slot>
</div>
<div class="modal-footer">
<slot name="footer">
<button
type="button"
class="btn"
data-dismiss="modal"
@click="emitCancel($event)"
>
{{ s__('Modal|Cancel') }}
</button>
<button
type="button"
class="btn"
:class="`btn-${footerPrimaryButtonVariant}`"
data-dismiss="modal"
@click="emitSubmit($event)"
>
{{ footerPrimaryButtonText }}
</button>
</slot>
</div>
</div>
</div>
</div>
</template>
...@@ -261,8 +261,6 @@ ul.controls { ...@@ -261,8 +261,6 @@ ul.controls {
} }
.author_link { .author_link {
display: inline-block;
.avatar-inline { .avatar-inline {
margin-left: 0; margin-left: 0;
margin-right: 0; margin-right: 0;
......
...@@ -321,11 +321,11 @@ ul.related-merge-requests > li { ...@@ -321,11 +321,11 @@ ul.related-merge-requests > li {
} }
.issue-token-state-icon-open { .issue-token-state-icon-open {
color: $green-600; color: $green-500;
} }
.issue-token-state-icon-closed { .issue-token-state-icon-closed {
color: $red-600; color: $blue-500;
} }
.issue-token-title { .issue-token-title {
......
...@@ -39,12 +39,12 @@ class Projects::Clusters::GcpController < Projects::ApplicationController ...@@ -39,12 +39,12 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
def verify_billing def verify_billing
case google_project_billing_status case google_project_billing_status
when 'true' when nil
return
when 'false'
flash[:alert] = _('Please <a href=%{link_to_billing} target="_blank" rel="noopener noreferrer">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again.').html_safe % { link_to_billing: "https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral" }
else
flash[:alert] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.') flash[:alert] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.')
when false
flash[:alert] = _('Please <a href=%{link_to_billing} target="_blank" rel="noopener noreferrer">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again.').html_safe % { link_to_billing: "https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral" }
when true
return
end end
@cluster = ::Clusters::Cluster.new(create_params) @cluster = ::Clusters::Cluster.new(create_params)
...@@ -81,9 +81,7 @@ class Projects::Clusters::GcpController < Projects::ApplicationController ...@@ -81,9 +81,7 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
end end
def google_project_billing_status def google_project_billing_status
Gitlab::Redis::SharedState.with do |redis| CheckGcpProjectBillingWorker.get_billing_state(token_in_session)
redis.get(CheckGcpProjectBillingWorker.redis_shared_state_key_for(token_in_session))
end
end end
def token_in_session def token_in_session
......
...@@ -44,12 +44,41 @@ module Ci ...@@ -44,12 +44,41 @@ module Ci
scope :unstarted, ->() { where(runner_id: nil) } scope :unstarted, ->() { where(runner_id: nil) }
scope :ignore_failures, ->() { where(allow_failure: false) } scope :ignore_failures, ->() { where(allow_failure: false) }
# This convoluted mess is because we need to handle two cases of
# artifact files during the migration. And a simple OR clause
# makes it impossible to optimize.
# Instead we want to use UNION ALL and do two carefully
# constructed disjoint queries. But Rails cannot handle UNION or
# UNION ALL queries so we do the query in a subquery and wrap it
# in an otherwise redundant WHERE IN query (IN is fine for
# non-null columns).
# This should all be ripped out when the migration is finished and
# replaced with just the new storage to avoid the extra work.
scope :with_artifacts, ->() do scope :with_artifacts, ->() do
where('(artifacts_file IS NOT NULL AND artifacts_file <> ?) OR EXISTS (?)', old = Ci::Build.select(:id).where(%q[artifacts_file <> ''])
'', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id')) new = Ci::Build.select(:id).where(%q[(artifacts_file IS NULL OR artifacts_file = '') AND EXISTS (?)],
Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id'))
where('ci_builds.id IN (? UNION ALL ?)', old, new)
end end
scope :with_artifacts_not_expired, ->() { with_artifacts.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
scope :with_expired_artifacts, ->() { with_artifacts.where('artifacts_expire_at < ?', Time.now) } scope :with_artifacts_not_expired, ->() do
old = Ci::Build.select(:id).where(%q[artifacts_file <> '' AND (artifacts_expire_at IS NULL OR artifacts_expire_at > ?)], Time.now)
new = Ci::Build.select(:id).where(%q[(artifacts_file IS NULL OR artifacts_file = '') AND EXISTS (?)],
Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id AND (expire_at IS NULL OR expire_at > ?)', Time.now))
where('ci_builds.id IN (? UNION ALL ?)', old, new)
end
scope :with_expired_artifacts, ->() do
old = Ci::Build.select(:id).where(%q[artifacts_file <> '' AND artifacts_expire_at < ?], Time.now)
new = Ci::Build.select(:id).where(%q[(artifacts_file IS NULL OR artifacts_file = '') AND EXISTS (?)],
Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id AND expire_at < ?', Time.now))
where('ci_builds.id IN (? UNION ALL ?)', old, new)
end
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) } scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) } scope :manual_actions, ->() { where(when: :manual, status: COMPLETED_STATUSES + [:manual]) }
scope :ref_protected, -> { where(protected: true) } scope :ref_protected, -> { where(protected: true) }
......
...@@ -102,10 +102,6 @@ class Event < ActiveRecord::Base ...@@ -102,10 +102,6 @@ class Event < ActiveRecord::Base
self.inheritance_column = 'action' self.inheritance_column = 'action'
# "data" will be removed in 10.0 but it may be possible that JOINs happen that
# include this column, hence we're ignoring it as well.
ignore_column :data
class << self class << self
def model_name def model_name
ActiveModel::Name.new(self, nil, 'event') ActiveModel::Name.new(self, nil, 'event')
......
...@@ -39,7 +39,7 @@ class ExternalIssue ...@@ -39,7 +39,7 @@ class ExternalIssue
end end
def to_reference(_from = nil, full: nil) def to_reference(_from = nil, full: nil)
id reference_link_text
end end
def reference_link_text(from = nil) def reference_link_text(from = nil)
......
...@@ -11,6 +11,7 @@ class Identity < ActiveRecord::Base ...@@ -11,6 +11,7 @@ class Identity < ActiveRecord::Base
validates :user_id, uniqueness: { scope: :provider } validates :user_id, uniqueness: { scope: :provider }
before_save :ensure_normalized_extern_uid, if: :extern_uid_changed? before_save :ensure_normalized_extern_uid, if: :extern_uid_changed?
after_destroy :clear_user_synced_attributes, if: :user_synced_attributes_metadata_from_provider?
scope :with_provider, ->(provider) { where(provider: provider) } scope :with_provider, ->(provider) { where(provider: provider) }
scope :with_extern_uid, ->(provider, extern_uid) do scope :with_extern_uid, ->(provider, extern_uid) do
...@@ -36,4 +37,12 @@ class Identity < ActiveRecord::Base ...@@ -36,4 +37,12 @@ class Identity < ActiveRecord::Base
self.extern_uid = Identity.normalize_uid(self.provider, self.extern_uid) self.extern_uid = Identity.normalize_uid(self.provider, self.extern_uid)
end end
def user_synced_attributes_metadata_from_provider?
user.user_synced_attributes_metadata&.provider == provider
end
def clear_user_synced_attributes
user.user_synced_attributes_metadata&.destroy
end
end end
...@@ -600,7 +600,15 @@ class Repository ...@@ -600,7 +600,15 @@ class Repository
def license_key def license_key
return unless exists? return unless exists?
Licensee.license(path).try(:key) # The licensee gem creates a Rugged object from the path:
# https://github.com/benbalter/licensee/blob/v8.7.0/lib/licensee/projects/git_project.rb
begin
Licensee.license(path).try(:key)
# Normally we would rescue Rugged::Error, but that is banned by lint-rugged
# and we need to migrate this endpoint to Gitaly:
# https://gitlab.com/gitlab-org/gitaly/issues/1026
rescue
end
end end
cache_method :license_key cache_method :license_key
......
...@@ -258,7 +258,7 @@ class User < ActiveRecord::Base ...@@ -258,7 +258,7 @@ class User < ActiveRecord::Base
def find_for_database_authentication(warden_conditions) def find_for_database_authentication(warden_conditions)
conditions = warden_conditions.dup conditions = warden_conditions.dup
if login = conditions.delete(:login) if login = conditions.delete(:login)
where(conditions).find_by("lower(username) = :value OR lower(email) = :value", value: login.downcase) where(conditions).find_by("lower(username) = :value OR lower(email) = :value", value: login.downcase.strip)
else else
find_by(conditions) find_by(conditions)
end end
......
...@@ -19,19 +19,10 @@ module Issues ...@@ -19,19 +19,10 @@ module Issues
# on rewriting notes (unfolding references) # on rewriting notes (unfolding references)
# #
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
# New issue tasks
#
@new_issue = create_new_issue @new_issue = create_new_issue
rewrite_notes update_new_issue
rewrite_issue_award_emoji update_old_issue
add_note_moved_from
# Old issue tasks
#
add_note_moved_to
close_issue
mark_as_moved
end end
notify_participants notify_participants
...@@ -41,6 +32,18 @@ module Issues ...@@ -41,6 +32,18 @@ module Issues
private private
def update_new_issue
rewrite_notes
rewrite_issue_award_emoji
add_note_moved_from
end
def update_old_issue
add_note_moved_to
close_issue
mark_as_moved
end
def create_new_issue def create_new_issue
new_params = { id: nil, iid: nil, label_ids: cloneable_label_ids, new_params = { id: nil, iid: nil, label_ids: cloneable_label_ids,
milestone_id: cloneable_milestone_id, milestone_id: cloneable_milestone_id,
......
...@@ -135,7 +135,7 @@ module MergeRequests ...@@ -135,7 +135,7 @@ module MergeRequests
end end
def append_closes_description def append_closes_description
return unless issue return unless issue&.to_reference.present?
closes_issue = "Closes #{issue.to_reference}" closes_issue = "Closes #{issue.to_reference}"
...@@ -164,7 +164,7 @@ module MergeRequests ...@@ -164,7 +164,7 @@ module MergeRequests
return if merge_request.title.present? return if merge_request.title.present?
if issue_iid.present? if issue_iid.present?
merge_request.title = "Resolve #{issue_iid}" merge_request.title = "Resolve #{issue.to_reference}"
branch_title = source_branch.downcase.remove(issue_iid.downcase).titleize.humanize branch_title = source_branch.downcase.remove(issue_iid.downcase).titleize.humanize
merge_request.title += " \"#{branch_title}\"" if branch_title.present? merge_request.title += " \"#{branch_title}\"" if branch_title.present?
end end
......
...@@ -5,11 +5,15 @@ module Projects ...@@ -5,11 +5,15 @@ module Projects
end end
def execute def execute
params[:file] = Gitlab::ProjectTemplate.find(params[:template_name]).file template_name = params.delete(:template_name)
file = Gitlab::ProjectTemplate.find(template_name).file
params[:file] = file
GitlabProjectsImportService.new(current_user, params).execute
GitlabProjectsImportService.new(@current_user, @params).execute
ensure ensure
params[:file]&.close file&.close
end end
end end
end end
...@@ -11,12 +11,14 @@ module Projects ...@@ -11,12 +11,14 @@ module Projects
def execute def execute
FileUtils.mkdir_p(File.dirname(import_upload_path)) FileUtils.mkdir_p(File.dirname(import_upload_path))
file = params.delete(:file)
FileUtils.copy_entry(file.path, import_upload_path) FileUtils.copy_entry(file.path, import_upload_path)
Gitlab::ImportExport::ProjectCreator.new(params[:namespace_id], params[:import_type] = 'gitlab_project'
current_user, params[:import_source] = import_upload_path
import_upload_path,
params[:path]).execute ::Projects::CreateService.new(current_user, params).execute
end end
private private
...@@ -28,9 +30,5 @@ module Projects ...@@ -28,9 +30,5 @@ module Projects
def tmp_filename def tmp_filename
SecureRandom.hex SecureRandom.hex
end end
def file
params[:file]
end
end end
end end
...@@ -7,10 +7,9 @@ ...@@ -7,10 +7,9 @@
- build_path_proc = ->(scope) { admin_jobs_path(scope: scope) } - build_path_proc = ->(scope) { admin_jobs_path(scope: scope) }
= render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope = render "shared/builds/tabs", build_path_proc: build_path_proc, all_builds: @all_builds, scope: @scope
.nav-controls - if @all_builds.running_or_pending.any?
- if @all_builds.running_or_pending.any? #stop-jobs-modal
#stop-jobs-modal .nav-controls
%button#stop-jobs-button.btn.btn-danger{ data: { toggle: 'modal', %button#stop-jobs-button.btn.btn-danger{ data: { toggle: 'modal',
target: '#stop-jobs-modal', target: '#stop-jobs-modal',
url: cancel_all_admin_jobs_path } } url: cancel_all_admin_jobs_path } }
......
...@@ -65,7 +65,7 @@ ...@@ -65,7 +65,7 @@
- if @project.avatar? - if @project.avatar?
%hr %hr
= link_to _('Remove avatar'), project_avatar_path(@project), data: { confirm: _("Avatar will be removed. Are you sure?") }, method: :delete, class: "btn btn-danger btn-inverted" = link_to _('Remove avatar'), project_avatar_path(@project), data: { confirm: _("Avatar will be removed. Are you sure?") }, method: :delete, class: "btn btn-danger btn-inverted"
= f.submit 'Save changes', class: "btn btn-success" = f.submit 'Save changes', class: "btn btn-success js-btn-save-general-project-settings"
%section.settings.sharing-permissions.no-animate{ class: ('expanded' if expanded) } %section.settings.sharing-permissions.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
......
- @no_container = true - @no_container = true
- breadcrumb_title "CI / CD Charts" - breadcrumb_title "CI / CD Charts"
- page_title _("Charts"), _("Pipelines") - page_title _("Charts"), _("Pipelines")
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_d3')
= page_specific_javascript_bundle_tag('graphs')
%div{ class: container_class } %div{ class: container_class }
.sub-header-block .sub-header-block
......
- content_for :page_specific_javascripts do
= webpack_bundle_tag('pipelines_times')
%div %div
%p.light %p.light
= _("Commit duration in minutes for last 30 commits") = _("Commit duration in minutes for last 30 commits")
......
- content_for :page_specific_javascripts do
= webpack_bundle_tag('pipelines_charts')
%h4= _("Pipelines charts") %h4= _("Pipelines charts")
%p %p
&nbsp; &nbsp;
......
- content_for :page_specific_javascripts do
= webpack_bundle_tag('integrations')
.row.prepend-top-default.append-bottom-default .row.prepend-top-default.append-bottom-default
.col-lg-3 .col-lg-3
%h4.prepend-top-0 %h4.prepend-top-0
......
...@@ -6,4 +6,4 @@ ...@@ -6,4 +6,4 @@
$(".project-edit-errors").html("#{escape_javascript(render('errors'))}"); $(".project-edit-errors").html("#{escape_javascript(render('errors'))}");
$('.save-project-loader').hide(); $('.save-project-loader').hide();
$('.project-edit-container').show(); $('.project-edit-container').show();
$('.edit-project .btn-save').enable(); $('.edit-project .js-btn-save-general-project-settings').enable();
...@@ -23,11 +23,11 @@ ...@@ -23,11 +23,11 @@
- if show_archive_options - if show_archive_options
%li.divider %li.divider
%li.js-filter-archived-projects %li.js-filter-archived-projects
= link_to group_children_path(@group, archived: nil), class: ("is-active" unless params[:archived].present?) do = link_to filter_groups_path(archived: nil), class: ("is-active" unless params[:archived].present?) do
Hide archived projects Hide archived projects
%li.js-filter-archived-projects %li.js-filter-archived-projects
= link_to group_children_path(@group, archived: true), class: ("is-active" if Gitlab::Utils.to_boolean(params[:archived])) do = link_to filter_groups_path(archived: true), class: ("is-active" if Gitlab::Utils.to_boolean(params[:archived])) do
Show archived projects Show archived projects
%li.js-filter-archived-projects %li.js-filter-archived-projects
= link_to group_children_path(@group, archived: 'only'), class: ("is-active" if params[:archived] == 'only') do = link_to filter_groups_path(archived: 'only'), class: ("is-active" if params[:archived] == 'only') do
Show archived projects only Show archived projects only
...@@ -4,9 +4,6 @@ ...@@ -4,9 +4,6 @@
- page_description @user.bio - page_description @user.bio
- header_title @user.name, user_path(@user) - header_title @user.name, user_path(@user)
- @no_container = true - @no_container = true
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_d3'
= webpack_bundle_tag 'users'
= content_for :meta_tags do = content_for :meta_tags do
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity") = auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
......
...@@ -7,6 +7,7 @@ class CheckGcpProjectBillingWorker ...@@ -7,6 +7,7 @@ class CheckGcpProjectBillingWorker
LEASE_TIMEOUT = 3.seconds.to_i LEASE_TIMEOUT = 3.seconds.to_i
SESSION_KEY_TIMEOUT = 5.minutes SESSION_KEY_TIMEOUT = 5.minutes
BILLING_TIMEOUT = 1.hour BILLING_TIMEOUT = 1.hour
BILLING_CHANGED_LABELS = { state_transition: nil }.freeze
def self.get_session_token(token_key) def self.get_session_token(token_key)
Gitlab::Redis::SharedState.with do |redis| Gitlab::Redis::SharedState.with do |redis|
...@@ -22,8 +23,11 @@ class CheckGcpProjectBillingWorker ...@@ -22,8 +23,11 @@ class CheckGcpProjectBillingWorker
end end
end end
def self.redis_shared_state_key_for(token) def self.get_billing_state(token)
"gitlab:gcp:#{Digest::SHA1.hexdigest(token)}:billing_enabled" Gitlab::Redis::SharedState.with do |redis|
value = redis.get(redis_shared_state_key_for(token))
ActiveRecord::Type::Boolean.new.type_cast_from_user(value)
end
end end
def perform(token_key) def perform(token_key)
...@@ -33,12 +37,9 @@ class CheckGcpProjectBillingWorker ...@@ -33,12 +37,9 @@ class CheckGcpProjectBillingWorker
return unless token return unless token
return unless try_obtain_lease_for(token) return unless try_obtain_lease_for(token)
billing_enabled_projects = CheckGcpProjectBillingService.new.execute(token) billing_enabled_state = !CheckGcpProjectBillingService.new.execute(token).empty?
Gitlab::Redis::SharedState.with do |redis| update_billing_change_counter(self.class.get_billing_state(token), billing_enabled_state)
redis.set(self.class.redis_shared_state_key_for(token), self.class.set_billing_state(token, billing_enabled_state)
!billing_enabled_projects.empty?,
ex: BILLING_TIMEOUT)
end
end end
private private
...@@ -51,9 +52,41 @@ class CheckGcpProjectBillingWorker ...@@ -51,9 +52,41 @@ class CheckGcpProjectBillingWorker
"gitlab:gcp:session:#{token_key}" "gitlab:gcp:session:#{token_key}"
end end
def self.redis_shared_state_key_for(token)
"gitlab:gcp:#{Digest::SHA1.hexdigest(token)}:billing_enabled"
end
def self.set_billing_state(token, value)
Gitlab::Redis::SharedState.with do |redis|
redis.set(redis_shared_state_key_for(token), value, ex: BILLING_TIMEOUT)
end
end
def try_obtain_lease_for(token) def try_obtain_lease_for(token)
Gitlab::ExclusiveLease Gitlab::ExclusiveLease
.new("check_gcp_project_billing_worker:#{token.hash}", timeout: LEASE_TIMEOUT) .new("check_gcp_project_billing_worker:#{token.hash}", timeout: LEASE_TIMEOUT)
.try_obtain .try_obtain
end end
def billing_changed_counter
@billing_changed_counter ||= Gitlab::Metrics.counter(
:gcp_billing_change_count,
"Counts the number of times a GCP project changed billing_enabled state from false to true",
BILLING_CHANGED_LABELS
)
end
def state_transition(previous_state, current_state)
if previous_state.nil? && !current_state
'no_billing'
elsif previous_state.nil? && current_state
'with_billing'
elsif !previous_state && current_state
'billing_configured'
end
end
def update_billing_change_counter(previous_state, current_state)
billing_changed_counter.increment(state_transition: state_transition(previous_state, current_state))
end
end end
---
title: Fix license expiration duration to show trial info only for trial license
merge_request: 4573
author:
type: fixed
---
title: Improve SAST description for no new vulnerabilities
merge_request:
author:
type: fixed
---
title: Prevent MR Widget error when no CI configured
merge_request:
author:
type: fixed
---
title: Fix Teleporting Emoji
merge_request: 16963
author: Jared Deckard <jared.deckard@gmail.com>
type: fixed
---
title: "Fix user avatar's vertical align on the issues and merge requests pages"
merge_request: 17072
author: Laszlo Karpati
type: fixed
title: Fix 404 when listing archived projects in a group where all projects have been archived
merge_request: 17077
author: Ashley Dumaine
type: fixed
---
title: Hide CI secret variable values after saving
merge_request: 17044
author:
type: changed
---
title: Allows project rename after validation error
merge_request: 17150
author:
type: fixed
---
title: Remove whitespace from the username/email sign in form field
merge_request: 17020
author: Peter lauck
type: changed
---
title: Escape HTML entities in commit messages
merge_request:
author:
type: fixed
---
title: Change SQL for expired artifacts to use new ci_job_artifacts.expire_at
merge_request: 16578
author:
type: performance
---
title: Respect description and visibility when creating project from template
merge_request: 16820
author: George Tsiolis
type: fixed
---
title: Fix validation of environment scope of variables
merge_request:
author:
type: fixed
---
title: Fixed error 500 when removing an identity with synced attributes and visiting
the profile page
merge_request: 17054
author:
type: fixed
---
title: Fixed bug with unauthenticated requests through git ssh
merge_request: 17149
author:
type: fixed
---
title: 'Ensure users cannot create environments with leading or trailing slashes (Fixes #39885)'
merge_request: 15273
author:
type: fixed
---
title: Add new modal Vue component
merge_request: 17108
author:
type: changed
...@@ -83,6 +83,7 @@ module Gitlab ...@@ -83,6 +83,7 @@ module Gitlab
# - Webhook URLs (:hook) # - Webhook URLs (:hook)
# - Sentry DSN (:sentry_dsn) # - Sentry DSN (:sentry_dsn)
# - Deploy keys (:key) # - Deploy keys (:key)
# - Secret variable values (:value)
config.filter_parameters += [/token$/, /password/, /secret/] config.filter_parameters += [/token$/, /password/, /secret/]
config.filter_parameters += %i( config.filter_parameters += %i(
certificate certificate
...@@ -94,6 +95,7 @@ module Gitlab ...@@ -94,6 +95,7 @@ module Gitlab
sentry_dsn sentry_dsn
trace trace
variables variables
value
) )
# Enable escaping HTML in JSON. # Enable escaping HTML in JSON.
......
...@@ -26,6 +26,7 @@ class Rack::Attack ...@@ -26,6 +26,7 @@ class Rack::Attack
throttle('throttle_unauthenticated', Gitlab::Throttle.unauthenticated_options) do |req| throttle('throttle_unauthenticated', Gitlab::Throttle.unauthenticated_options) do |req|
Gitlab::Throttle.settings.throttle_unauthenticated_enabled && Gitlab::Throttle.settings.throttle_unauthenticated_enabled &&
req.unauthenticated? && req.unauthenticated? &&
!req.api_internal_request? &&
req.ip req.ip
end end
...@@ -54,6 +55,10 @@ class Rack::Attack ...@@ -54,6 +55,10 @@ class Rack::Attack
path.start_with?('/api') path.start_with?('/api')
end end
def api_internal_request?
path =~ %r{^/api/v\d+/internal/}
end
def web_request? def web_request?
!api_request? !api_request?
end end
......
...@@ -75,7 +75,6 @@ var config = { ...@@ -75,7 +75,6 @@ var config = {
issues: './issues/issues_bundle.js', issues: './issues/issues_bundle.js',
how_to_merge: './how_to_merge.js', how_to_merge: './how_to_merge.js',
issue_show: './issue_show/index.js', issue_show: './issue_show/index.js',
integrations: './integrations',
job_details: './jobs/job_details_bundle.js', job_details: './jobs/job_details_bundle.js',
ldap_group_links: './groups/ldap_group_links.js', ldap_group_links: './groups/ldap_group_links.js',
locale: './locale/index.js', locale: './locale/index.js',
...@@ -88,9 +87,7 @@ var config = { ...@@ -88,9 +87,7 @@ var config = {
notes: './notes/index.js', notes: './notes/index.js',
pdf_viewer: './blob/pdf_viewer.js', pdf_viewer: './blob/pdf_viewer.js',
pipelines: './pipelines/pipelines_bundle.js', pipelines: './pipelines/pipelines_bundle.js',
pipelines_charts: './pipelines/pipelines_charts.js',
pipelines_details: './pipelines/pipeline_details_bundle.js', pipelines_details: './pipelines/pipeline_details_bundle.js',
pipelines_times: './pipelines/pipelines_times.js',
profile: './profile/profile_bundle.js', profile: './profile/profile_bundle.js',
project_import_gl: './projects/project_import_gitlab_project.js', project_import_gl: './projects/project_import_gitlab_project.js',
prometheus_metrics: './prometheus_metrics', prometheus_metrics: './prometheus_metrics',
...@@ -115,7 +112,6 @@ var config = { ...@@ -115,7 +112,6 @@ var config = {
vue_merge_request_widget: './vue_merge_request_widget/index.js', vue_merge_request_widget: './vue_merge_request_widget/index.js',
test: './test.js', test: './test.js',
two_factor_auth: './two_factor_auth.js', two_factor_auth: './two_factor_auth.js',
users: './users/index.js',
webpack_runtime: './webpack.js', webpack_runtime: './webpack.js',
}, },
......
...@@ -9,7 +9,6 @@ Gitlab::Seeder.quiet do ...@@ -9,7 +9,6 @@ Gitlab::Seeder.quiet do
s.username = 'root' s.username = 'root'
s.password = '5iveL!fe' s.password = '5iveL!fe'
s.admin = true s.admin = true
s.projects_limit = 100
s.confirmed_at = DateTime.now s.confirmed_at = DateTime.now
end end
end end
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class OptimizeCiJobArtifacts < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
disable_ddl_transaction!
def up
# job_id is just here to be a covering index for index only scans
# since we'll almost always be joining against ci_builds on job_id
add_concurrent_index(:ci_job_artifacts, [:expire_at, :job_id])
add_concurrent_index(:ci_builds, [:artifacts_expire_at], where: "artifacts_file <> ''")
end
def down
remove_concurrent_index(:ci_job_artifacts, [:expire_at, :job_id])
remove_concurrent_index(:ci_builds, [:artifacts_expire_at], where: "artifacts_file <> ''")
end
end
...@@ -370,6 +370,7 @@ ActiveRecord::Schema.define(version: 20180208183958) do ...@@ -370,6 +370,7 @@ ActiveRecord::Schema.define(version: 20180208183958) do
t.integer "failure_reason" t.integer "failure_reason"
end end
add_index "ci_builds", ["artifacts_expire_at"], name: "index_ci_builds_on_artifacts_expire_at", where: "(artifacts_file <> ''::text)", using: :btree
add_index "ci_builds", ["auto_canceled_by_id"], name: "index_ci_builds_on_auto_canceled_by_id", using: :btree add_index "ci_builds", ["auto_canceled_by_id"], name: "index_ci_builds_on_auto_canceled_by_id", using: :btree
add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
add_index "ci_builds", ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree add_index "ci_builds", ["commit_id", "status", "type"], name: "index_ci_builds_on_commit_id_and_status_and_type", using: :btree
...@@ -411,6 +412,7 @@ ActiveRecord::Schema.define(version: 20180208183958) do ...@@ -411,6 +412,7 @@ ActiveRecord::Schema.define(version: 20180208183958) do
t.integer "file_store" t.integer "file_store"
end end
add_index "ci_job_artifacts", ["expire_at", "job_id"], name: "index_ci_job_artifacts_on_expire_at_and_job_id", using: :btree
add_index "ci_job_artifacts", ["job_id", "file_type"], name: "index_ci_job_artifacts_on_job_id_and_file_type", unique: true, using: :btree add_index "ci_job_artifacts", ["job_id", "file_type"], name: "index_ci_job_artifacts_on_job_id_and_file_type", unique: true, using: :btree
add_index "ci_job_artifacts", ["project_id"], name: "index_ci_job_artifacts_on_project_id", using: :btree add_index "ci_job_artifacts", ["project_id"], name: "index_ci_job_artifacts_on_project_id", using: :btree
......
...@@ -9,7 +9,19 @@ created in snippets, wikis, and repos. ...@@ -9,7 +9,19 @@ created in snippets, wikis, and repos.
## PlantUML Server ## PlantUML Server
Before you can enable PlantUML in GitLab; you need to set up your own PlantUML Before you can enable PlantUML in GitLab; you need to set up your own PlantUML
server that will generate the diagrams. Installing and configuring your server that will generate the diagrams.
### Docker
With Docker, you can just run a container like this:
`docker run -d --name plantuml -p 8080:8080 plantuml/plantuml-server:tomcat`
The **PlantUML URL** will be the hostname of the server running the container.
### Debian/Ubuntu
Installing and configuring your
own PlantUML server is easy in Debian/Ubuntu distributions using Tomcat. own PlantUML server is easy in Debian/Ubuntu distributions using Tomcat.
First you need to create a `plantuml.war` file from the source code: First you need to create a `plantuml.war` file from the source code:
......
...@@ -69,7 +69,7 @@ new one, and attempting to pull a repo. ...@@ -69,7 +69,7 @@ new one, and attempting to pull a repo.
> **Warning:** Do not disable writes until SSH is confirmed to be working > **Warning:** Do not disable writes until SSH is confirmed to be working
perfectly, because the file will quickly become out-of-date. perfectly, because the file will quickly become out-of-date.
In the case of lookup failures (which are not uncommon), the `authorized_keys` In the case of lookup failures (which are common), the `authorized_keys`
file will still be scanned. So git SSH performance will still be slow for many file will still be scanned. So git SSH performance will still be slow for many
users as long as a large file exists. users as long as a large file exists.
......
...@@ -61,6 +61,21 @@ Before proceeding with the Pages configuration, you will need to: ...@@ -61,6 +61,21 @@ Before proceeding with the Pages configuration, you will need to:
NOTE: **Note:** NOTE: **Note:**
If your GitLab instance and the Pages daemon are deployed in a private network or behind a firewall, your GitLab Pages websites will only be accessible to devices/users that have access to the private network. If your GitLab instance and the Pages daemon are deployed in a private network or behind a firewall, your GitLab Pages websites will only be accessible to devices/users that have access to the private network.
### Add the domain to the Public Suffix List
The [Public Suffix List](https://publicsuffix.org) is used by browsers to
decide how to treat subdomains. If your GitLab instance allows members of the
public to create GitLab Pages sites, it also allows those users to create
subdomains on the pages domain (`example.io`). Adding the domain to the Public
Suffix List prevents browsers from accepting
[supercookies](https://en.wikipedia.org/wiki/HTTP_cookie#Supercookie),
among other things.
Follow [these instructions](https://publicsuffix.org/submit/) to submit your
GitLab Pages subdomain. For instance, if your domain is `example.io`, you should
request that `*.example.io` is added to the Public Suffix List. GitLab.com
added `*.gitlab.io` [in 2016](https://gitlab.com/gitlab-com/infrastructure/issues/230).
### DNS configuration ### DNS configuration
GitLab Pages expect to run on their own virtual host. In your DNS server/provider GitLab Pages expect to run on their own virtual host. In your DNS server/provider
......
...@@ -71,9 +71,10 @@ learn how to leverage its potential even more. ...@@ -71,9 +71,10 @@ learn how to leverage its potential even more.
and status of each CI environment running on Kubernetes and status of each CI environment running on Kubernetes
- [Trigger pipelines through the GitLab API](triggers/README.md) - [Trigger pipelines through the GitLab API](triggers/README.md)
- [Trigger pipelines on a schedule](../user/project/pipelines/schedules.md) - [Trigger pipelines on a schedule](../user/project/pipelines/schedules.md)
- [Deploy Boards](../user/project/deploy_boards.md) - Check the current health
- [Kubernetes clusters](../user/project/clusters/index.md) - Integrate one or - [Kubernetes clusters](../user/project/clusters/index.md) - Integrate one or
more Kubernetes clusters to your project more Kubernetes clusters to your project
- [Deploy Boards](../user/project/deploy_boards.md) - Check the current health
and status of each CI/CD environment running on Kubernetes
## GitLab CI/CD for Docker ## GitLab CI/CD for Docker
......
# Components
## Contents
* [Dropdowns](#dropdowns)
* [Modals](#modals)
## Dropdowns
See also the [corresponding UX guide](../ux_guide/components.md#dropdowns).
### How to style a bootstrap dropdown
1. Use the HTML structure provided by the [docs][bootstrap-dropdowns]
1. Add a specific class to the top level `.dropdown` element
```Haml
.dropdown.my-dropdown
%button{ type: 'button', data: { toggle: 'dropdown' }, 'aria-haspopup': true, 'aria-expanded': false }
%span.dropdown-toggle-text
Toggle Dropdown
= icon('chevron-down')
%ul.dropdown-menu
%li
%a
item!
```
Or use the helpers
```Haml
.dropdown.my-dropdown
= dropdown_toggle('Toogle!', { toggle: 'dropdown' })
= dropdown_content
%li
%a
item!
```
[bootstrap-dropdowns]: https://getbootstrap.com/docs/3.3/javascript/#dropdowns
## Modals
See also the [corresponding UX guide](../ux_guide/components.md#modals).
We have a reusable Vue component for modals: [vue_shared/components/gl-modal.vue](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/vue_shared/components/gl-modal.vue)
Here is an example of how to use it:
```html
<gl-modal
id="dogs-out-modal"
:header-title-text="s__('ModalExample|Let the dogs out?')"
footer-primary-button-variant="danger"
:footer-primary-button-text="s__('ModalExample|Let them out')"
@submit="letOut(theDogs)"
>
{{ s__('ModalExample|You’re about to let the dogs out.') }}
</gl-modal>
```
![example modal](img/gl-modal.png)
# Dropdowns This page has moved [here](components.md#dropdowns).
## How to style a bootstrap dropdown
1. Use the HTML structure provided by the [docs][bootstrap-dropdowns]
1. Add a specific class to the top level `.dropdown` element
```Haml
.dropdown.my-dropdown
%button{ type: 'button', data: { toggle: 'dropdown' }, 'aria-haspopup': true, 'aria-expanded': false }
%span.dropdown-toggle-text
Toggle Dropdown
= icon('chevron-down')
%ul.dropdown-menu
%li
%a
item!
```
Or use the helpers
```Haml
.dropdown.my-dropdown
= dropdown_toggle('Toogle!', { toggle: 'dropdown' })
= dropdown_content
%li
%a
item!
```
[bootstrap-dropdowns]: https://getbootstrap.com/docs/3.3/javascript/#dropdowns
...@@ -21,6 +21,8 @@ Working with our frontend assets requires Node (v4.3 or greater) and Yarn ...@@ -21,6 +21,8 @@ Working with our frontend assets requires Node (v4.3 or greater) and Yarn
[jQuery][jquery] is used throughout the application's JavaScript, with [jQuery][jquery] is used throughout the application's JavaScript, with
[Vue.js][vue] for particularly advanced, dynamic elements. [Vue.js][vue] for particularly advanced, dynamic elements.
We also use [Axios][axios] to handle all of our network requests.
### Browser Support ### Browser Support
For our currently-supported browsers, see our [requirements][requirements]. For our currently-supported browsers, see our [requirements][requirements].
...@@ -77,8 +79,10 @@ Axios specific practices and gotchas. ...@@ -77,8 +79,10 @@ Axios specific practices and gotchas.
## [Icons](icons.md) ## [Icons](icons.md)
How we use SVG for our Icons. How we use SVG for our Icons.
## [Dropdowns](dropdowns.md) ## [Components](components.md)
How we use dropdowns.
How we use UI components.
--- ---
## Style Guides ## Style Guides
...@@ -122,6 +126,7 @@ The [externalization part of the guide](../i18n/externalization.md) explains the ...@@ -122,6 +126,7 @@ The [externalization part of the guide](../i18n/externalization.md) explains the
[webpack]: https://webpack.js.org/ [webpack]: https://webpack.js.org/
[jquery]: https://jquery.com/ [jquery]: https://jquery.com/
[vue]: http://vuejs.org/ [vue]: http://vuejs.org/
[axios]: https://github.com/axios/axios
[airbnb-js-style-guide]: https://github.com/airbnb/javascript [airbnb-js-style-guide]: https://github.com/airbnb/javascript
[scss-lint]: https://github.com/brigade/scss-lint [scss-lint]: https://github.com/brigade/scss-lint
[install]: ../../install/installation.md#4-node [install]: ../../install/installation.md#4-node
......
# Manage feature flags # Manage feature flags
Starting from GitLab 9.3 we support feature flags via Starting from GitLab 9.3 we support feature flags for features in GitLab via
[Flipper](https://github.com/jnunemaker/flipper/). You should use the `Feature` [Flipper](https://github.com/jnunemaker/flipper/). You should use the `Feature`
class (defined in `lib/feature.rb`) in your code to get, set and list feature class (defined in `lib/feature.rb`) in your code to get, set and list feature
flags. flags.
...@@ -19,3 +19,8 @@ dynamic (querying the DB etc.). ...@@ -19,3 +19,8 @@ dynamic (querying the DB etc.).
Once defined in `lib/feature.rb`, you will be able to activate a Once defined in `lib/feature.rb`, you will be able to activate a
feature for a given feature group via the [`feature_group` param of the features API](../api/features.md#set-or-create-a-feature) feature for a given feature group via the [`feature_group` param of the features API](../api/features.md#set-or-create-a-feature)
## Feature flags for user applications
GitLab does not yet support the use of feature flags in deployed user applications.
You can follow the progress on that [in the issue on our issue tracker](https://gitlab.com/gitlab-org/gitlab-ee/issues/779).
\ No newline at end of file
...@@ -40,37 +40,12 @@ See [Translation guidelines](translation.md). ...@@ -40,37 +40,12 @@ See [Translation guidelines](translation.md).
### Proof reading ### Proof reading
Proof reading helps ensure the accuracy and consistency of translations. Proof reading helps ensure the accuracy and consistency of translations. All
All translations are proof read before being accepted. translations are proof read before being accepted. If a translations requires
If a translations requires changes, you will be notified with a comment explaining why. changes, you will be notified with a comment explaining why.
Community assistance proof reading translations is encouraged and appreciated. See [Proofreading Translations](proofreader.md) for more information on who's
Requests to become a proof reader will be considered on the merits of previous translations. able to proofread and instructions on becoming a proofreader yourself.
- Bulgarian
- Chinese Simplified
- [Huang Tao](https://crowdin.com/profile/htve)
- Chinese Traditional
- [Huang Tao](https://crowdin.com/profile/htve)
- Chinese Traditional, Hong Kong
- [Huang Tao](https://crowdin.com/profile/htve)
- Dutch
- Esperanto
- French
- German
- Italian
- [Paolo Falomo](https://crowdin.com/profile/paolo.falomo)
- Japanese
- Korean
- [Huang Tao](https://crowdin.com/profile/htve)
- Portuguese, Brazilian
- Russian
- [Alexy Lustin](https://crowdin.com/profile/lustin)
- [Nikita Grylov](https://crowdin.com/profile/nixel2007)
- Spanish
- Ukrainian
If you would like to be added as a proof reader, please [open an issue](https://gitlab.com/gitlab-org/gitlab-ce/issues).
## Release ## Release
......
# Proofread Translations
Most translations are contributed, reviewed, and accepted by the community. We
are very appreciative of the work done by translators and proofreaders!
## Proofreaders
- Bulgarian
- Chinese Simplified
- Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
- Chinese Traditional
- Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
- Chinese Traditional, Hong Kong
- Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
- Dutch
- Esperanto
- French
- German
- Italian
- Paolo Falomo - [GitLab](https://gitlab.com/paolofalomo), [Crowdin](https://crowdin.com/profile/paolo.falomo)
- Japanese
- Korean
- Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
- Portuguese, Brazilian
- Paulo George Gomes Bezerra - [GitLab](https://gitlab.com/paulobezerra), [Crowdin](https://crowdin.com/profile/paulogomes.rep)
- Russian
- Nikita Grylov - [GitLab](https://gitlab.com/nixel2007), [Crowdin](https://crowdin.com/profile/nixel2007)
- Alexy Lustin - [GitLab](https://gitlab.com/allustin), [Crowdin](https://crowdin.com/profile/lustin)
- Spanish
- Ukrainian
- Volodymyr Sobotovych - [GitLab](https://gitlab.com/wheleph), [Crowdin](https://crowdin.com/profile/wheleph)
- Andrew Vityuk - [GitLab](https://gitlab.com/3_1_3_u), [Crowdin](https://crowdin.com/profile/andruwa13)
## Become a proofreader
> **Note:** Before requesting Proofreader permissions in Crowdin please make
> sure that you have a history of contributing translations to the GitLab
> project.
1. Once your translations have been accepted,
[open a merge request](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/new)
to request Proofreader permissions and add yourself to the list above.
In the merge request description, please include links to any projects you
have previously translated.
1. Your request to become a proofreader will be considered on the merits of
your previous translations.
...@@ -27,6 +27,17 @@ Gitlab::Profiler.profile('/my-user') ...@@ -27,6 +27,17 @@ Gitlab::Profiler.profile('/my-user')
# Returns a RubyProf::Profile where 100 seconds is spent in UsersController#show # Returns a RubyProf::Profile where 100 seconds is spent in UsersController#show
``` ```
For routes that require authorization you will need to provide a user to
`Gitlab::Profiler`. You can do this like so:
```ruby
Gitlab::Profiler.profile('/gitlab-org/gitlab-test', user: User.first)
```
The user you provide will need to have a [personal access
token](https://docs.gitlab.com/ce/user/profile/personal_access_tokens.html) in
the GitLab instance.
Passing a `logger:` keyword argument to `Gitlab::Profiler.profile` will send Passing a `logger:` keyword argument to `Gitlab::Profiler.profile` will send
ActiveRecord and ActionController log output to that logger. Further options are ActiveRecord and ActionController log output to that logger. Further options are
documented with the method source. documented with the method source.
......
...@@ -95,7 +95,9 @@ Auto Deploy, and Auto Monitoring will be silently skipped. ...@@ -95,7 +95,9 @@ Auto Deploy, and Auto Monitoring will be silently skipped.
The Auto DevOps base domain is required if you want to make use of [Auto The Auto DevOps base domain is required if you want to make use of [Auto
Review Apps](#auto-review-apps) and [Auto Deploy](#auto-deploy). It is defined Review Apps](#auto-review-apps) and [Auto Deploy](#auto-deploy). It is defined
under the project's CI/CD settings while [enabling Auto DevOps](#enabling-auto-devops). either under the project's CI/CD settings while
[enabling Auto DevOps](#enabling-auto-devops) or in instance-wide settings in
the CI/CD section.
It can also be set at the project or group level as a variable, `AUTO_DEVOPS_DOMAIN`. It can also be set at the project or group level as a variable, `AUTO_DEVOPS_DOMAIN`.
A wildcard DNS A record matching the base domain is required, for example, A wildcard DNS A record matching the base domain is required, for example,
......
This diff is collapsed.
...@@ -18,7 +18,7 @@ documentation. ...@@ -18,7 +18,7 @@ documentation.
> **Important:** > **Important:**
For security reasons, when using the command line, we strongly recommend For security reasons, when using the command line, we strongly recommend
you to [connect with GitLab via SSH](../../../ssh/README.md). that you [connect with GitLab via SSH](../../../ssh/README.md).
## Files ## Files
......
import initLDAPGroupsSelect from 'ee/ldap_groups_select';
export default initLDAPGroupsSelect;
...@@ -65,7 +65,7 @@ export default { ...@@ -65,7 +65,7 @@ export default {
}; };
}, },
showApproveButton() { showApproveButton() {
return this.userCanApprove && !this.userHasApproved; return this.userCanApprove && !this.userHasApproved && this.mr.isOpen;
}, },
showSuggestedApprovers() { showSuggestedApprovers() {
return this.suggestedApprovers && this.suggestedApprovers.length; return this.suggestedApprovers && this.suggestedApprovers.length;
......
...@@ -117,8 +117,8 @@ export default { ...@@ -117,8 +117,8 @@ export default {
}, },
securityText() { securityText() {
const { newIssues, resolvedIssues } = this.mr.securityReport; const { newIssues, resolvedIssues, allIssues } = this.mr.securityReport;
return this.sastText(newIssues, resolvedIssues); return this.sastText(newIssues, resolvedIssues, allIssues);
}, },
dockerText() { dockerText() {
......
...@@ -2,11 +2,13 @@ import { s__, n__, __, sprintf } from '~/locale'; ...@@ -2,11 +2,13 @@ import { s__, n__, __, sprintf } from '~/locale';
export default { export default {
methods: { methods: {
sastText(newIssues = [], resolvedIssues = []) { sastText(newIssues = [], resolvedIssues = [], allIssues = []) {
const text = []; const text = [];
if (!newIssues.length && !resolvedIssues.length) { if (!newIssues.length && !resolvedIssues.length && !allIssues.length) {
text.push(s__('ciReport|SAST detected no security vulnerabilities')); text.push(s__('ciReport|SAST detected no security vulnerabilities'));
} else if (!newIssues.length && !resolvedIssues.length && allIssues.length) {
text.push(s__('ciReport|SAST detected no new security vulnerabilities'));
} else if (newIssues.length || resolvedIssues.length) { } else if (newIssues.length || resolvedIssues.length) {
text.push(s__('ciReport|SAST')); text.push(s__('ciReport|SAST'));
} }
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
%span.light #{label}: %span.light #{label}:
%strong= value %strong= value
.panel.panel-default .panel.panel-default.js-license-info-panel
.panel-heading .panel-heading
Details Details
%ul.well-list %ul.well-list
...@@ -39,9 +39,11 @@ ...@@ -39,9 +39,11 @@
- else - else
Expires: Expires:
- if @license.will_expire? && @license.active? - if @license.will_expire? && @license.active?
%strong= time_ago_with_tooltip(@license.expires_at)
- if @license.trial? - if @license.trial?
%span Free trial will expire in #{pluralize(@license.remaining_days, 'day')} %strong.has-tooltip{ title: @license.expires_at.to_formatted_s(:long), data: { placement: 'top' } }
Free trial will expire in #{pluralize(@license.remaining_days, 'day')}
- else
%strong= time_ago_with_tooltip(@license.expires_at)
- else - else
%strong Never %strong Never
......
...@@ -27,6 +27,30 @@ feature "License Admin" do ...@@ -27,6 +27,30 @@ feature "License Admin" do
end end
end end
context 'with a trial license' do
let!(:license) { create(:license, trial: true) }
it 'shows expiration duration with license type' do
visit admin_license_path
page.within '.js-license-info-panel' do
expect(page).to have_content('Expires: Free trial will expire in')
end
end
end
context 'with a regular license' do
let!(:license) { create(:license) }
it 'shows only expiration duration' do
visit admin_license_path
page.within '.js-license-info-panel' do
expect(page).not_to have_content('Expires: Free trial will expire in')
end
end
end
context 'with an expired trial license' do context 'with an expired trial license' do
let!(:license) { create(:license, trial: true, expired: true) } let!(:license) { create(:license, trial: true, expired: true) }
......
...@@ -81,7 +81,7 @@ feature 'EE Clusters' do ...@@ -81,7 +81,7 @@ feature 'EE Clusters' do
.to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s) .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing) allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing)
allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return('true') allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return(true)
allow_any_instance_of(GoogleApi::CloudPlatform::Client) allow_any_instance_of(GoogleApi::CloudPlatform::Client)
.to receive(:projects_zones_clusters_create) do .to receive(:projects_zones_clusters_create) do
......
...@@ -32,10 +32,11 @@ describe 'User approves a merge request', :js do ...@@ -32,10 +32,11 @@ describe 'User approves a merge request', :js do
before do before do
project.add_developer(user2) project.add_developer(user2)
project.add_developer(user3) project.add_developer(user3)
visit(merge_request_path(merge_request))
end end
it 'shows multiple approvers beyond the needed count' do it 'shows multiple approvers beyond the needed count' do
visit(merge_request_path(merge_request))
click_button('Approve') click_button('Approve')
wait_for_requests wait_for_requests
...@@ -47,6 +48,19 @@ describe 'User approves a merge request', :js do ...@@ -47,6 +48,19 @@ describe 'User approves a merge request', :js do
expect(all('.js-approver-list-member').count).to eq(3) expect(all('.js-approver-list-member').count).to eq(3)
end end
it "doesn't show the approve additionally when a merge request is closed" do
merge_request_closed = create(:merge_request, :closed, source_project: project, target_project: project)
create(:approval, merge_request: merge_request_closed, user: user)
sign_in(user2)
visit(merge_request_path(merge_request_closed))
wait_for_requests
expect(page).not_to have_button('Approve')
expect(page).not_to have_button('Approve additionally')
end
def sign_in_visit_merge_request(user) def sign_in_visit_merge_request(user)
sign_in(user) sign_in(user)
visit(merge_request_path(merge_request)) visit(merge_request_path(merge_request))
......
Feature: Group Members
Background:
Given I sign in as "John Doe"
And "John Doe" is owner of group "Owned"
And "John Doe" is guest of group "Guest"
Scenario: Search member by name
Given "Mary Jane" is guest of group "Guest"
And I visit group "Guest" members page
When I search for 'Mary' member
Then I should see user "Mary Jane" in team list
Then I should not see user "John Doe" in team list
Feature: Group Milestones
Background:
Given I sign in as "John Doe"
And "John Doe" is owner of group "Owned"
Scenario: I should see group "Owned" milestone index page with no milestones
When I visit group "Owned" page
And I click on group milestones
Then I should see group milestones index page has no milestones
Scenario: I should see group "Owned" milestone index page with milestones
Given Group has projects with milestones
When I visit group "Owned" page
And I click on group milestones
Then I should see group milestones index page with milestones
Scenario: I should see group "Owned" milestone show page
Given Group has projects with milestones
When I visit group "Owned" page
And I click on group milestones
And I click on one group milestone
Then I should see group milestone with descriptions and expiry date
And I should see group milestone with all issues and MRs assigned to that milestone
Scenario: Create group milestones
Given I visit group "Owned" milestones page
And I click new milestone button
And I fill milestone name
When I press create mileston button
Then group milestone should be created
Scenario: I should see Issues listed with labels
Given Group has projects with milestones
When I visit group "Owned" page
And I click on group milestones
And I click on one group milestone
Then I should see the "bug" label
And I should see the "feature" label
And I should see the project name in the Issue row
@javascript
Scenario: I should see the Labels tab
Given Group has projects with milestones
When I visit group "Owned" page
And I click on group milestones
And I click on one group milestone
And I click on the "Labels" tab
Then I should see the list of labels
@profile
Feature: Profile
Background:
Given I sign in as a user
Scenario: I look at my profile
Given I visit profile page
Then I should see my profile info
@javascript
Scenario: I can see groups I belong to
Given I have group with projects
When I visit profile page
And I click on my profile picture
Then I should see my user page
And I should see groups I belong to
Scenario: I edit profile
Given I visit profile page
Then I change my profile info
And I should see new profile info
Scenario: I change my password without old one
Given I visit profile password page
When I try change my password w/o old one
Then I should see a missing password error message
And I should be redirected to password page
Scenario: I change my password
Given I visit profile password page
Then I change my password
And I should be redirected to sign in page
Scenario: I edit my avatar
Given I visit profile page
Then I change my avatar
And I should see new avatar
And I should see the "Remove avatar" button
And I should see the gravatar host link
Scenario: I remove my avatar
Given I visit profile page
And I have an avatar
When I remove my avatar
Then I should see my gravatar
And I should not see the "Remove avatar" button
And I should see the gravatar host link
Scenario: My password is expired
Given my password is expired
And I am not an ldap user
Given I visit profile password page
Then I redirected to expired password page
And I submit new password
And I redirected to sign in page
Scenario: I unsuccessfully change my password
Given I visit profile password page
When I unsuccessfully change my password
Then I should see a password error message
Scenario: I visit history tab
Given I logout
And I sign in via the UI
And I have activity
When I visit Authentication log page
Then I should see my activity
Scenario: I visit my user page
When I visit profile page
And I click on my profile picture
Then I should see my user page
Scenario: I can manage application
Given I visit profile applications page
Then I should see application form
Then I fill application form out and submit
And I see application
Then I click edit
And I see edit application form
Then I change name of application and submit
And I see that application was changed
Then I visit profile applications page
And I click to remove application
Then I see that application is removed
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment