Commit 09122f93 authored by Shinya Maeda's avatar Shinya Maeda

Merge branch 'master' into per-project-pipeline-iid

parents 1d20679e 5b1416aa
......@@ -6,7 +6,7 @@ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.7-golang-1.9-git
- gitlab-org
.default-cache: &default-cache
key: "ruby-2.3.7-with-yarn"
key: "ruby-2.3.7-debian-stretch-with-yarn"
paths:
- vendor/ruby
- .yarn-cache/
......@@ -550,7 +550,7 @@ static-analysis:
script:
- scripts/static-analysis
cache:
key: "ruby-2.3.7-with-yarn-and-rubocop"
key: "ruby-2.3.7-debian-stretch-with-yarn-and-rubocop"
paths:
- vendor/ruby
- .yarn-cache/
......
......@@ -2,6 +2,15 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 10.8.2 (2018-05-28)
### Security (3 changes)
- Prevent user passwords from being changed without providing the previous password.
- Fix API to remove deploy key from project instead of deleting it entirely.
- Fixed bug that allowed importing arbitrary project attributes.
## 10.8.1 (2018-05-23)
### Fixed (9 changes)
......@@ -193,6 +202,15 @@ entry.
- Gitaly handles repository forks by default.
## 10.7.5 (2018-05-28)
### Security (3 changes)
- Prevent user passwords from being changed without providing the previous password.
- Fix API to remove deploy key from project instead of deleting it entirely.
- Fixed bug that allowed importing arbitrary project attributes.
## 10.7.4 (2018-05-21)
### Fixed (1 change)
......@@ -457,6 +475,16 @@ entry.
- Upgrade Gitaly to upgrade its charlock_holmes.
## 10.6.6 (2018-05-28)
### Security (4 changes)
- Do not allow non-members to create MRs via forked projects when MRs are private.
- Prevent user passwords from being changed without providing the previous password.
- Fix API to remove deploy key from project instead of deleting it entirely.
- Fixed bug that allowed importing arbitrary project attributes.
## 10.6.5 (2018-04-24)
### Security (1 change)
......
......@@ -181,7 +181,7 @@ Team labels specify what team is responsible for this issue.
Assigning a team label makes sure issues get the attention of the appropriate
people.
The current team labels are ~Build, ~"CI/CD", ~Discussion, ~Documentation, ~Quality,
The current team labels are ~Distribution, ~"CI/CD", ~Discussion, ~Documentation, ~Quality,
~Geo, ~Gitaly, ~Monitoring, ~Platform, ~Release, ~"Security Products" and ~"UX".
The descriptions on the [labels page][labels-page] explain what falls under the
......
......@@ -133,7 +133,7 @@ gem 'gitlab-markup', '~> 1.6.2'
gem 'redcarpet', '~> 3.4'
gem 'commonmarker', '~> 0.17'
gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~> 4.2'
gem 'rdoc', '~> 6.0'
gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1'
......@@ -162,7 +162,7 @@ gem 'acts-as-taggable-on', '~> 5.0'
# Background jobs
gem 'sidekiq', '~> 5.1'
gem 'sidekiq-cron', '~> 0.6.0'
gem 'redis-namespace', '~> 1.5.2'
gem 'redis-namespace', '~> 1.6.0'
gem 'sidekiq-limit_fetch', '~> 3.4', require: false
# Cron Parser
......@@ -412,7 +412,7 @@ group :ed25519 do
end
# Gitaly GRPC client
gem 'gitaly-proto', '~> 0.99.0', require: 'gitaly'
gem 'gitaly-proto', '~> 0.100.0', require: 'gitaly'
gem 'grpc', '~> 1.11.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
......
......@@ -281,7 +281,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gitaly-proto (0.99.0)
gitaly-proto (0.100.0)
google-protobuf (~> 3.1)
grpc (~> 1.10)
github-linguist (5.3.3)
......@@ -694,8 +694,7 @@ GEM
ffi
rbnacl-libsodium (1.0.11)
rbnacl (>= 3.0.1)
rdoc (4.2.2)
json (~> 1.4)
rdoc (6.0.4)
re2 (1.1.1)
recaptcha (3.0.0)
json
......@@ -709,8 +708,8 @@ GEM
redis-activesupport (5.0.4)
activesupport (>= 3, < 6)
redis-store (>= 1.3, < 2)
redis-namespace (1.5.2)
redis (~> 3.0, >= 3.0.4)
redis-namespace (1.6.0)
redis (>= 3.0.4)
redis-rack (2.0.4)
rack (>= 1.5, < 3)
redis-store (>= 1.2, < 2)
......@@ -801,7 +800,7 @@ GEM
rubyzip (1.2.1)
rufus-scheduler (3.4.0)
et-orbi (~> 1.0)
rugged (0.27.0)
rugged (0.27.1)
safe_yaml (1.0.4)
sanitize (2.1.0)
nokogiri (>= 1.4.4)
......@@ -918,7 +917,7 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.5)
unicode-display_width (1.3.0)
unicode-display_width (1.3.2)
unicorn (5.1.0)
kgio (~> 2.6)
raindrops (~> 0.7)
......@@ -1036,7 +1035,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.99.0)
gitaly-proto (~> 0.100.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
......@@ -1124,12 +1123,12 @@ DEPENDENCIES
rblineprof (~> 0.3.6)
rbnacl (~> 4.0)
rbnacl-libsodium
rdoc (~> 4.2)
rdoc (~> 6.0)
re2 (~> 1.1.1)
recaptcha (~> 3.0)
redcarpet (~> 3.4)
redis (~> 3.2)
redis-namespace (~> 1.5.2)
redis-namespace (~> 1.6.0)
redis-rails (~> 5.0.2)
request_store (~> 1.3)
responders (~> 2.0)
......
......@@ -160,7 +160,7 @@ export default {
@input="debouncedPreview"
/>
<span
class="help-block"
class="form-text text-muted"
v-html="helpText"
></span>
</div>
......@@ -176,7 +176,7 @@ export default {
@input="debouncedPreview"
/>
<span
class="help-block"
class="form-text text-muted"
v-html="helpText"
></span>
</div>
......
......@@ -43,7 +43,7 @@ export default {
return `${this.changedIcon}-solid`;
},
changedIconClass() {
return `multi-${this.changedIcon} pull-left`;
return `multi-${this.changedIcon} float-left`;
},
tooltipTitle() {
if (!this.showTooltip) return undefined;
......
......@@ -144,14 +144,14 @@ export default {
<loading-button
:loading="submitCommitLoading"
:disabled="commitButtonDisabled"
container-class="btn btn-success btn-sm pull-left"
container-class="btn btn-success btn-sm float-left"
:label="__('Commit')"
@click="commitChanges"
/>
<button
v-if="!discardDraftButtonDisabled"
type="button"
class="btn btn-default btn-sm pull-right"
class="btn btn-default btn-sm float-right"
@click="discardDraft"
>
{{ __('Discard draft') }}
......@@ -159,7 +159,7 @@ export default {
<button
v-else
type="button"
class="btn btn-default btn-sm pull-right"
class="btn btn-default btn-sm float-right"
@click="toggleIsSmall"
>
{{ __('Collapse') }}
......
......@@ -120,7 +120,7 @@ export default {
</ul>
<p
v-else
class="multi-file-commit-list help-block"
class="multi-file-commit-list form-text text-muted"
>
{{ __('No changes') }}
</p>
......
......@@ -80,7 +80,7 @@ export default {
{{ __('Commit Message') }}
<span
v-popover="$options.popoverOptions"
class="help-block prepend-left-10"
class="form-text text-muted prepend-left-10"
>
<icon
name="question"
......
......@@ -72,21 +72,19 @@ export default {
<form
slot="body"
@submit.prevent="createEntryInStore"
class="form-group row append-bottom-0"
class="form-group row"
>
<fieldset class="form-group append-bottom-0">
<label class="label-light col-form-label col-sm-3 ide-new-modal-label">
{{ __('Name') }}
</label>
<div class="col-sm-9">
<input
type="text"
class="form-control"
v-model="entryName"
ref="fieldName"
/>
</div>
</fieldset>
<label class="label-light col-form-label col-sm-3">
{{ __('Name') }}
</label>
<div class="col-sm-9">
<input
type="text"
class="form-control"
v-model="entryName"
ref="fieldName"
/>
</div>
</form>
</deprecated-modal>
</template>
......@@ -169,7 +169,7 @@ export default {
:show-tooltip="true"
:show-staged-icon="true"
:force-modified-icon="true"
class="pull-right"
class="float-right"
/>
</span>
<new-dropdown
......
......@@ -48,11 +48,10 @@ export default {
return `${this.job.runner.description} (#${this.job.runner.id})`;
},
retryButtonClass() {
let className = 'js-retry-button pull-right btn btn-retry d-none d-md-block d-lg-block d-xl-block';
let className =
'js-retry-button float-right btn btn-retry d-none d-md-block d-lg-block d-xl-block';
className +=
this.job.status && this.job.recoverable
? ' btn-primary'
: ' btn-inverted-secondary';
this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary';
return className;
},
hasTimeout() {
......@@ -104,8 +103,7 @@ export default {
<button
type="button"
:aria-label="__('Toggle Sidebar')"
class="btn btn-blank gutter-toggle pull-right
d-block d-sm-block d-md-none js-sidebar-build-toggle"
class="btn btn-blank gutter-toggle float-right d-block d-md-none js-sidebar-build-toggle"
>
<i
aria-hidden="true"
......
......@@ -362,7 +362,7 @@ export default class MergeRequestTabs {
//
// status - Boolean, true to show, false to hide
toggleLoading(status) {
$('.mr-loading-status .loading').toggleClass('hidden', status);
$('.mr-loading-status .loading').toggleClass('hidden', !status);
}
diffViewType() {
......
import groupAvatar from '~/group_avatar';
import TransferDropdown from '~/groups/transfer_dropdown';
import initConfirmDangerModal from '~/confirm_danger_modal';
import initSettingsPanels from '~/settings_panels';
document.addEventListener('DOMContentLoaded', () => {
groupAvatar();
new TransferDropdown(); // eslint-disable-line no-new
initConfirmDangerModal();
});
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
initSettingsPanels();
});
import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
document.addEventListener('DOMContentLoaded', () => {
initGkeDropdowns();
});
......@@ -213,7 +213,7 @@
</i>
</div>
</div>
<span class="help-block">{{ visibilityLevelDescription }}</span>
<span class="form-text text-muted">{{ visibilityLevelDescription }}</span>
<label
v-if="visibilityLevel !== visibilityOptions.PRIVATE"
class="request-access"
......
import bp from '../../../breakpoints';
import { slugify } from '../../../lib/utils/text_utility';
import { parseQueryStringIntoObject } from '../../../lib/utils/common_utils';
import { mergeUrlParams, redirectTo } from '../../../lib/utils/url_utility';
export default class Wikis {
constructor() {
......@@ -28,7 +30,12 @@ export default class Wikis {
if (slug.length > 0) {
const wikisPath = slugInput.getAttribute('data-wikis-path');
window.location.href = `${wikisPath}/${slug}`;
// If the wiki is empty, we need to merge the current URL params to keep the "create" view.
const params = parseQueryStringIntoObject(window.location.search.substr(1));
const url = mergeUrlParams(params, `${wikisPath}/${slug}`);
redirectTo(url);
e.preventDefault();
}
}
......
......@@ -77,10 +77,9 @@ export default class UserTabs {
this.action = action || this.defaultAction;
this.$parentEl = $(parentEl) || $(document);
this.windowLocation = window.location;
this.$parentEl.find('.nav-links a')
.each((i, navLink) => {
this.loaded[$(navLink).attr('data-action')] = false;
});
this.$parentEl.find('.nav-links a').each((i, navLink) => {
this.loaded[$(navLink).attr('data-action')] = false;
});
this.actions = Object.keys(this.loaded);
this.bindEvents();
......@@ -116,8 +115,7 @@ export default class UserTabs {
}
activateTab(action) {
return this.$parentEl.find(`.nav-links .js-${action}-tab a`)
.tab('show');
return this.$parentEl.find(`.nav-links .js-${action}-tab a`).tab('show');
}
setTab(action, endpoint) {
......@@ -137,7 +135,8 @@ export default class UserTabs {
loadTab(action, endpoint) {
this.toggleLoading(true);
return axios.get(endpoint)
return axios
.get(endpoint)
.then(({ data }) => {
const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html);
......@@ -161,10 +160,11 @@ export default class UserTabs {
const utcOffset = $calendarWrap.data('utcOffset');
let utcFormatted = 'UTC';
if (utcOffset !== 0) {
utcFormatted = `UTC${utcOffset > 0 ? '+' : ''}${(utcOffset / 3600)}`;
utcFormatted = `UTC${utcOffset > 0 ? '+' : ''}${utcOffset / 3600}`;
}
axios.get(calendarPath)
axios
.get(calendarPath)
.then(({ data }) => {
$calendarWrap.html(CALENDAR_TEMPLATE);
$calendarWrap.find('.calendar-hint').append(`(Timezone: ${utcFormatted})`);
......@@ -180,17 +180,20 @@ export default class UserTabs {
}
toggleLoading(status) {
return this.$parentEl.find('.loading-status .loading')
.toggleClass('hidden', status);
return this.$parentEl.find('.loading-status .loading').toggleClass('hidden', !status);
}
setCurrentAction(source) {
let newState = source;
newState = newState.replace(/\/+$/, '');
newState += this.windowLocation.search + this.windowLocation.hash;
history.replaceState({
url: newState,
}, document.title, newState);
history.replaceState(
{
url: newState,
},
document.title,
newState,
);
return newState;
}
......
......@@ -99,7 +99,7 @@ Please update your Git repository remotes as soon as possible.`),
:disabled="isRequestPending"
/>
</div>
<p class="help-block">
<p class="form-text text-muted">
{{ path }}
</p>
</div>
......
import _ from 'underscore';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
import DropdownSearchInput from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
import store from '../store';
export default {
store,
components: {
LoadingIcon,
DropdownButton,
DropdownSearchInput,
DropdownHiddenInput,
},
props: {
fieldId: {
type: String,
required: true,
},
fieldName: {
type: String,
required: true,
},
defaultValue: {
type: String,
required: false,
default: '',
},
},
data() {
return {
isLoading: false,
hasErrors: false,
searchQuery: '',
gapiError: '',
};
},
computed: {
results() {
if (!this.items) {
return [];
}
return this.items.filter(item => item.name.toLowerCase().indexOf(this.searchQuery) > -1);
},
},
methods: {
fetchSuccessHandler() {
if (this.defaultValue) {
const itemToSelect = _.find(this.items, item => item.name === this.defaultValue);
if (itemToSelect) {
this.setItem(itemToSelect.name);
}
}
this.isLoading = false;
this.hasErrors = false;
},
fetchFailureHandler(resp) {
this.isLoading = false;
this.hasErrors = true;
if (resp.result && resp.result.error) {
this.gapiError = resp.result.error.message;
}
},
},
};
<script>
import { sprintf, s__ } from '~/locale';
import { mapState, mapGetters, mapActions } from 'vuex';
import gkeDropdownMixin from './gke_dropdown_mixin';
export default {
name: 'GkeMachineTypeDropdown',
mixins: [gkeDropdownMixin],
computed: {
...mapState([
'isValidatingProjectBilling',
'projectHasBillingEnabled',
'selectedZone',
'selectedMachineType',
]),
...mapState({ items: 'machineTypes' }),
...mapGetters(['hasZone', 'hasMachineType']),
allDropdownsSelected() {
return this.projectHasBillingEnabled && this.hasZone && this.hasMachineType;
},
isDisabled() {
return (
this.isLoading ||
this.isValidatingProjectBilling ||
!this.projectHasBillingEnabled ||
!this.hasZone
);
},
toggleText() {
if (this.isLoading) {
return s__('ClusterIntegration|Fetching machine types');
}
if (this.selectedMachineType) {
return this.selectedMachineType;
}
if (!this.projectHasBillingEnabled && !this.hasZone) {
return s__('ClusterIntegration|Select project and zone to choose machine type');
}
return !this.hasZone
? s__('ClusterIntegration|Select zone to choose machine type')
: s__('ClusterIntegration|Select machine type');
},
errorMessage() {
return sprintf(
s__(
'ClusterIntegration|An error occured while trying to fetch zone machine types: %{error}',
),
{ error: this.gapiError },
);
},
},
watch: {
selectedZone() {
this.hasErrors = false;
if (this.hasZone) {
this.isLoading = true;
this.fetchMachineTypes()
.then(this.fetchSuccessHandler)
.catch(this.fetchFailureHandler);
}
},
selectedMachineType() {
this.enableSubmit();
},
},
methods: {
...mapActions(['fetchMachineTypes']),
...mapActions({ setItem: 'setMachineType' }),
enableSubmit() {
if (this.allDropdownsSelected) {
const submitButtonEl = document.querySelector('.js-gke-cluster-creation-submit');
if (submitButtonEl) {
submitButtonEl.removeAttribute('disabled');
}
}
},
},
};
</script>
<template>
<div>
<div
class="js-gcp-machine-type-dropdown dropdown"
:class="{ 'gl-show-field-errors': hasErrors }"
>
<dropdown-hidden-input
:name="fieldName"
:value="selectedMachineType"
/>
<dropdown-button
:class="{ 'gl-field-error-outline': hasErrors }"
:is-disabled="isDisabled"
:is-loading="isLoading"
:toggle-text="toggleText"
/>
<div class="dropdown-menu dropdown-select">
<dropdown-search-input
v-model="searchQuery"
:placeholder-text="s__('ClusterIntegration|Search machine types')"
/>
<div class="dropdown-content">
<ul>
<li v-show="!results.length">
<span class="menu-item">
{{ s__('ClusterIntegration|No machine types matched your search') }}
</span>
</li>
<li
v-for="result in results"
:key="result.id"
>
<button
type="button"
@click.prevent="setItem(result.name)"
>
{{ result.name }}
</button>
</li>
</ul>
</div>
<div class="dropdown-loading">
<loading-icon />
</div>
</div>
</div>
<span
class="form-text text-muted"
:class="{ 'gl-field-error': hasErrors }"
v-if="hasErrors"
>
{{ errorMessage }}
</span>
</div>
</template>
<script>
import _ from 'underscore';
import { s__, sprintf } from '~/locale';
import { mapState, mapGetters, mapActions } from 'vuex';
import gkeDropdownMixin from './gke_dropdown_mixin';
export default {
name: 'GkeProjectIdDropdown',
mixins: [gkeDropdownMixin],
props: {
docsUrl: {
type: String,
required: true,
},
},
computed: {
...mapState(['selectedProject', 'isValidatingProjectBilling', 'projectHasBillingEnabled']),
...mapState({ items: 'projects' }),
...mapGetters(['hasProject']),
hasOneProject() {
return this.items && this.items.length === 1;
},
isDisabled() {
return (
this.isLoading || this.isValidatingProjectBilling || (this.items && this.items.length < 2)
);
},
toggleText() {
if (this.isValidatingProjectBilling) {
return s__('ClusterIntegration|Validating project billing status');
}
if (this.isLoading) {
return s__('ClusterIntegration|Fetching projects');
}
if (this.hasProject) {
return this.selectedProject.name;
}
if (!this.items) {
return s__('ClusterIntegration|No projects found');
}
return s__('ClusterIntegration|Select project');
},
helpText() {
let message;
if (this.hasErrors) {
return this.errorMessage;
}
if (!this.items) {
message =
'ClusterIntegration|We were unable to fetch any projects. Ensure that you have a project on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.';
}
message =
this.items && this.items.length
? 'ClusterIntegration|To use a new project, first create one on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.'
: 'ClusterIntegration|To create a cluster, first create a project on %{docsLinkStart}Google Cloud Platform%{docsLinkEnd}.';
return sprintf(
s__(message),
{
docsLinkEnd: '&nbsp;<i class="fa fa-external-link" aria-hidden="true"></i></a>',
docsLinkStart: `<a href="${_.escape(
this.docsUrl,
)}" target="_blank" rel="noopener noreferrer">`,
},
false,
);
},
errorMessage() {
if (!this.projectHasBillingEnabled) {
if (this.gapiError) {
return s__(
'ClusterIntegration|We could not verify that one of your projects on GCP has billing enabled. Please try again.',
);
}
return sprintf(
s__(
'This project does not have billing enabled. To create a cluster, <a href=%{linkToBilling} target="_blank" rel="noopener noreferrer">enable billing <i class="fa fa-external-link" aria-hidden="true"></i></a> and try again.',
),
{
linkToBilling:
'https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral',
},
false,
);
}
return sprintf(
s__('ClusterIntegration|An error occured while trying to fetch your projects: %{error}'),
{ error: this.gapiError },
);
},
},
watch: {
selectedProject() {
this.setIsValidatingProjectBilling(true);
this.validateProjectBilling()
.then(this.validateProjectBillingSuccessHandler)
.catch(this.validateProjectBillingFailureHandler);
},
},
created() {
this.isLoading = true;
this.fetchProjects()
.then(this.fetchSuccessHandler)
.catch(this.fetchFailureHandler);
},
methods: {
...mapActions(['fetchProjects', 'setIsValidatingProjectBilling', 'validateProjectBilling']),
...mapActions({ setItem: 'setProject' }),
fetchSuccessHandler() {
if (this.defaultValue) {
const projectToSelect = _.find(this.items, item => item.projectId === this.defaultValue);
if (projectToSelect) {
this.setItem(projectToSelect);
}
} else if (this.items.length === 1) {
this.setItem(this.items[0]);
}
this.isLoading = false;
this.hasErrors = false;
},
validateProjectBillingSuccessHandler() {
this.hasErrors = !this.projectHasBillingEnabled;
},
validateProjectBillingFailureHandler(resp) {
this.hasErrors = true;
this.gapiError = resp.result ? resp.result.error.message : resp;
},
},
};
</script>
<template>
<div>
<div
class="js-gcp-project-id-dropdown dropdown"
:class="{ 'gl-show-field-errors': hasErrors }"
>
<dropdown-hidden-input
:name="fieldName"
:value="selectedProject.projectId"
/>
<dropdown-button
:class="{
'gl-field-error-outline': hasErrors,
'read-only': hasOneProject
}"
:is-disabled="isDisabled"
:is-loading="isLoading"
:toggle-text="toggleText"
/>
<div class="dropdown-menu dropdown-select">
<dropdown-search-input
v-model="searchQuery"
:placeholder-text="s__('ClusterIntegration|Search projects')"
/>
<div class="dropdown-content">
<ul>
<li v-show="!results.length">
<span class="menu-item">
{{ s__('ClusterIntegration|No projects matched your search') }}
</span>
</li>
<li
v-for="result in results"
:key="result.project_number"
>
<button
type="button"
@click.prevent="setItem(result)"
>
{{ result.name }}
</button>
</li>
</ul>
</div>
<div class="dropdown-loading">
<loading-icon />
</div>
</div>
</div>
<span
class="form-text text-muted"
:class="{ 'gl-field-error': hasErrors }"
v-html="helpText"
></span>
</div>
</template>
<script>
import { sprintf, s__ } from '~/locale';
import { mapState, mapActions } from 'vuex';
import gkeDropdownMixin from './gke_dropdown_mixin';
export default {
name: 'GkeZoneDropdown',
mixins: [gkeDropdownMixin],
computed: {
...mapState([
'selectedProject',
'selectedZone',
'projects',
'isValidatingProjectBilling',
'projectHasBillingEnabled',
]),
...mapState({ items: 'zones' }),
isDisabled() {
return this.isLoading || this.isValidatingProjectBilling || !this.projectHasBillingEnabled;
},
toggleText() {
if (this.isLoading) {
return s__('ClusterIntegration|Fetching zones');
}
if (this.selectedZone) {
return this.selectedZone;
}
return !this.projectHasBillingEnabled
? s__('ClusterIntegration|Select project to choose zone')
: s__('ClusterIntegration|Select zone');
},
errorMessage() {
return sprintf(
s__('ClusterIntegration|An error occured while trying to fetch project zones: %{error}'),
{ error: this.gapiError },
);
},
},
watch: {
isValidatingProjectBilling(isValidating) {
this.hasErrors = false;
if (!isValidating && this.projectHasBillingEnabled) {
this.isLoading = true;
this.fetchZones()
.then(this.fetchSuccessHandler)
.catch(this.fetchFailureHandler);
}
},
},
methods: {
...mapActions(['fetchZones']),
...mapActions({ setItem: 'setZone' }),
},
};
</script>
<template>
<div>
<div
class="js-gcp-zone-dropdown dropdown"
:class="{ 'gl-show-field-errors': hasErrors }"
>
<dropdown-hidden-input
:name="fieldName"
:value="selectedZone"
/>
<dropdown-button
:class="{ 'gl-field-error-outline': hasErrors }"
:is-disabled="isDisabled"
:is-loading="isLoading"
:toggle-text="toggleText"
/>
<div class="dropdown-menu dropdown-select">
<dropdown-search-input
v-model="searchQuery"
:placeholder-text="s__('ClusterIntegration|Search zones')"
/>
<div class="dropdown-content">
<ul>
<li v-show="!results.length">
<span class="menu-item">
{{ s__('ClusterIntegration|No zones matched your search') }}
</span>
</li>
<li
v-for="result in results"
:key="result.id"
>
<button
type="button"
@click.prevent="setItem(result.name)"
>
{{ result.name }}
</button>
</li>
</ul>
</div>
<div class="dropdown-loading">
<loading-icon />
</div>
</div>
</div>
<span
class="form-text text-muted"
:class="{ 'gl-field-error': hasErrors }"
v-if="hasErrors"
>
{{ errorMessage }}
</span>
</div>
</template>
import { s__ } from '~/locale';
export const GCP_API_ERROR = s__(
'ClusterIntegration|An error occurred when trying to contact the Google Cloud API. Please try again later.',
);
export const GCP_API_CLOUD_BILLING_ENDPOINT =
'https://www.googleapis.com/discovery/v1/apis/cloudbilling/v1/rest';
export const GCP_API_CLOUD_RESOURCE_MANAGER_ENDPOINT =
'https://www.googleapis.com/discovery/v1/apis/cloudresourcemanager/v1/rest';
export const GCP_API_COMPUTE_ENDPOINT =
'https://www.googleapis.com/discovery/v1/apis/compute/v1/rest';
/* global gapi */
import Vue from 'vue';
import Flash from '~/flash';
import GkeProjectIdDropdown from './components/gke_project_id_dropdown.vue';
import GkeZoneDropdown from './components/gke_zone_dropdown.vue';
import GkeMachineTypeDropdown from './components/gke_machine_type_dropdown.vue';
import * as CONSTANTS from './constants';
const mountComponent = (entryPoint, component, componentName, extraProps = {}) => {
const el = document.querySelector(entryPoint);
if (!el) return false;
const hiddenInput = el.querySelector('input');
return new Vue({
el,
components: {
[componentName]: component,
},
render: createElement =>
createElement(componentName, {
props: {
fieldName: hiddenInput.getAttribute('name'),
fieldId: hiddenInput.getAttribute('id'),
defaultValue: hiddenInput.value,
...extraProps,
},
}),
});
};
const mountGkeProjectIdDropdown = () => {
const entryPoint = '.js-gcp-project-id-dropdown-entry-point';
const el = document.querySelector(entryPoint);
mountComponent(entryPoint, GkeProjectIdDropdown, 'gke-project-id-dropdown', {
docsUrl: el.dataset.docsurl,
});
};
const mountGkeZoneDropdown = () => {
mountComponent('.js-gcp-zone-dropdown-entry-point', GkeZoneDropdown, 'gke-zone-dropdown');
};
const mountGkeMachineTypeDropdown = () => {
mountComponent(
'.js-gcp-machine-type-dropdown-entry-point',
GkeMachineTypeDropdown,
'gke-machine-type-dropdown',
);
};
const gkeDropdownErrorHandler = () => {
Flash(CONSTANTS.GCP_API_ERROR);
};
const initializeGapiClient = () => {
const el = document.querySelector('.js-gke-cluster-creation');
if (!el) return false;
return gapi.client
.init({
discoveryDocs: [
CONSTANTS.GCP_API_CLOUD_BILLING_ENDPOINT,
CONSTANTS.GCP_API_CLOUD_RESOURCE_MANAGER_ENDPOINT,
CONSTANTS.GCP_API_COMPUTE_ENDPOINT,
],
})
.then(() => {
gapi.client.setToken({ access_token: el.dataset.token });
mountGkeProjectIdDropdown();
mountGkeZoneDropdown();
mountGkeMachineTypeDropdown();
})
.catch(gkeDropdownErrorHandler);
};
const initGkeDropdowns = () => {
if (!gapi) {
gkeDropdownErrorHandler();
return false;
}
return gapi.load('client', initializeGapiClient);
};
export default initGkeDropdowns;
/* global gapi */
import * as types from './mutation_types';
const gapiResourceListRequest = ({ resource, params, commit, mutation, payloadKey }) =>
new Promise((resolve, reject) => {
const request = resource.list(params);
return request.then(
resp => {
const { result } = resp;
commit(mutation, result[payloadKey]);
resolve();
},
resp => {
reject(resp);
},
);
});
export const setProject = ({ commit }, selectedProject) => {
commit(types.SET_PROJECT, selectedProject);
};
export const setZone = ({ commit }, selectedZone) => {
commit(types.SET_ZONE, selectedZone);
};
export const setMachineType = ({ commit }, selectedMachineType) => {
commit(types.SET_MACHINE_TYPE, selectedMachineType);
};
export const setIsValidatingProjectBilling = ({ commit }, isValidatingProjectBilling) => {
commit(types.SET_IS_VALIDATING_PROJECT_BILLING, isValidatingProjectBilling);
};
export const fetchProjects = ({ commit }) =>
gapiResourceListRequest({
resource: gapi.client.cloudresourcemanager.projects,
params: {},
commit,
mutation: types.SET_PROJECTS,
payloadKey: 'projects',
});
export const validateProjectBilling = ({ dispatch, commit, state }) =>
new Promise((resolve, reject) => {
const request = gapi.client.cloudbilling.projects.getBillingInfo({
name: `projects/${state.selectedProject.projectId}`,
});
commit(types.SET_ZONE, '');
commit(types.SET_MACHINE_TYPE, '');
return request.then(
resp => {
const { billingEnabled } = resp.result;
commit(types.SET_PROJECT_BILLING_STATUS, !!billingEnabled);
dispatch('setIsValidatingProjectBilling', false);
resolve();
},
resp => {
dispatch('setIsValidatingProjectBilling', false);
reject(resp);
},
);
});
export const fetchZones = ({ commit, state }) =>
gapiResourceListRequest({
resource: gapi.client.compute.zones,
params: {
project: state.selectedProject.projectId,
},
commit,
mutation: types.SET_ZONES,
payloadKey: 'items',
});
export const fetchMachineTypes = ({ commit, state }) =>
gapiResourceListRequest({
resource: gapi.client.compute.machineTypes,
params: {
project: state.selectedProject.projectId,
zone: state.selectedZone,
},
commit,
mutation: types.SET_MACHINE_TYPES,
payloadKey: 'items',
});
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
export const hasProject = state => !!state.selectedProject.projectId;
export const hasZone = state => !!state.selectedZone;
export const hasMachineType = state => !!state.selectedMachineType;
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
import createState from './state';
Vue.use(Vuex);
export const createStore = () =>
new Vuex.Store({
actions,
getters,
mutations,
state: createState(),
});
export default createStore();
export const SET_PROJECT = 'SET_PROJECT';
export const SET_PROJECT_BILLING_STATUS = 'SET_PROJECT_BILLING_STATUS';
export const SET_IS_VALIDATING_PROJECT_BILLING = 'SET_IS_VALIDATING_PROJECT_BILLING';
export const SET_ZONE = 'SET_ZONE';
export const SET_MACHINE_TYPE = 'SET_MACHINE_TYPE';
export const SET_PROJECTS = 'SET_PROJECTS';
export const SET_ZONES = 'SET_ZONES';
export const SET_MACHINE_TYPES = 'SET_MACHINE_TYPES';
import * as types from './mutation_types';
export default {
[types.SET_PROJECT](state, selectedProject) {
Object.assign(state, { selectedProject });
},
[types.SET_IS_VALIDATING_PROJECT_BILLING](state, isValidatingProjectBilling) {
Object.assign(state, { isValidatingProjectBilling });
},
[types.SET_PROJECT_BILLING_STATUS](state, projectHasBillingEnabled) {
Object.assign(state, { projectHasBillingEnabled });
},
[types.SET_ZONE](state, selectedZone) {
Object.assign(state, { selectedZone });
},
[types.SET_MACHINE_TYPE](state, selectedMachineType) {
Object.assign(state, { selectedMachineType });
},
[types.SET_PROJECTS](state, projects) {
Object.assign(state, { projects });
},
[types.SET_ZONES](state, zones) {
Object.assign(state, { zones });
},
[types.SET_MACHINE_TYPES](state, machineTypes) {
Object.assign(state, { machineTypes });
},
};
export default () => ({
selectedProject: {
projectId: '',
name: '',
},
selectedZone: '',
selectedMachineType: '',
isValidatingProjectBilling: null,
projectHasBillingEnabled: null,
projects: [],
zones: [],
machineTypes: [],
});
<script>
import { __ } from '~/locale';
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
export default {
components: {
LoadingIcon,
},
props: {
isDisabled: {
type: Boolean,
required: false,
default: false,
},
isLoading: {
type: Boolean,
required: false,
default: false,
},
toggleText: {
type: String,
required: false,
default: __('Select'),
},
},
};
</script>
<template>
<button
class="dropdown-menu-toggle dropdown-menu-full-width"
type="button"
data-toggle="dropdown"
aria-expanded="false"
:disabled="isDisabled || isLoading"
>
<loading-icon
v-show="isLoading"
:inline="true"
/>
<span class="dropdown-toggle-text">
{{ toggleText }}
</span>
<span
class="dropdown-toggle-icon"
v-show="!isLoading"
>
<i
class="fa fa-chevron-down"
aria-hidden="true"
data-hidden="true"
></i>
</span>
</button>
</template>
......@@ -5,8 +5,8 @@ export default {
type: String,
required: true,
},
label: {
type: Object,
value: {
type: [Number, String],
required: true,
},
},
......@@ -17,6 +17,6 @@ export default {
<input
type="hidden"
:name="name"
:value="label.id"
:value="value"
/>
</template>
<script>
import { __ } from '~/locale';
export default {
props: {
placeholderText: {
type: String,
required: true,
default: __('Search'),
},
},
data() {
return { searchQuery: this.value };
},
watch: {
searchQuery(query) {
this.$emit('input', query);
},
},
};
</script>
<template>
<div class="dropdown-input">
<input
class="dropdown-input-field"
type="search"
v-model="searchQuery"
:placeholder="placeholderText"
autocomplete="off"
/>
<i
class="fa fa-search dropdown-input-search"
aria-hidden="true"
data-hidden="true"
>
</i>
<i
class="fa fa-times dropdown-input-clear js-dropdown-input-clear"
aria-hidden="true"
data-hidden="true"
role="button"
>
</i>
</div>
</template>
......@@ -2,13 +2,13 @@
import $ from 'jquery';
import { __ } from '~/locale';
import LabelsSelect from '~/labels_select';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import LoadingIcon from '../../loading_icon.vue';
import DropdownTitle from './dropdown_title.vue';
import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
import DropdownButton from './dropdown_button.vue';
import DropdownHiddenInput from './dropdown_hidden_input.vue';
import DropdownHeader from './dropdown_header.vue';
import DropdownSearchInput from './dropdown_search_input.vue';
import DropdownFooter from './dropdown_footer.vue';
......@@ -140,7 +140,7 @@ export default {
v-for="label in context.labels"
:key="label.id"
:name="hiddenInputName"
:label="label"
:value="label.id"
/>
<div
class="dropdown"
......
......@@ -131,6 +131,10 @@ table {
}
.card {
.card-title {
margin-bottom: 0;
}
&.card-without-border {
@extend .border-0;
}
......@@ -147,3 +151,7 @@ table {
.nav-tabs .nav-link {
border: 0;
}
pre code {
white-space: pre-wrap;
}
......@@ -63,6 +63,10 @@
border-radius: $border-radius-base;
white-space: nowrap;
&:disabled.read-only {
color: $gl-text-color !important;
}
&.no-outline {
outline: 0;
}
......
......@@ -2,7 +2,7 @@
* Well styled list
*
*/
.card-body-list {
.hover-list {
position: relative;
margin: 0;
padding: 0;
......
......@@ -405,7 +405,7 @@ table.u2f-registrations {
margin-right: $gl-padding / 4;
}
.label-verification-status {
.badge-verification-status {
border-width: 1px;
border-style: solid;
......
......@@ -546,7 +546,7 @@
margin-right: 0;
}
&.help-block {
&.form-text.text-muted {
margin-left: 0;
right: 0;
}
......@@ -952,7 +952,7 @@
height: 30px;
}
.help-block {
.form-text.text-muted {
margin-top: 2px;
color: $blue-500;
cursor: pointer;
......@@ -1088,10 +1088,6 @@
font-size: 12px;
}
.ide-new-modal-label {
line-height: 34px;
}
.multi-file-commit-panel-success-message {
position: absolute;
top: 61px;
......
class Admin::DashboardController < Admin::ApplicationController
include CountHelper
COUNTED_ITEMS = [Project, User, Group, ForkedProjectLink, Issue, MergeRequest,
Note, Snippet, Key, Milestone].freeze
def index
@counts = Gitlab::Database::Count.approximate_counts(COUNTED_ITEMS)
@projects = Project.order_id_desc.without_deleted.with_route.limit(10)
@users = User.order_id_desc.limit(10)
@groups = Group.order_id_desc.with_route.limit(10)
......
......@@ -93,8 +93,6 @@ class ProfilesController < Profiles::ApplicationController
:linkedin,
:location,
:name,
:password,
:password_confirmation,
:public_email,
:skype,
:twitter,
......
class Projects::Clusters::GcpController < Projects::ApplicationController
before_action :authorize_read_cluster!
before_action :authorize_google_api, except: [:login]
before_action :authorize_google_project_billing, only: [:new, :create]
before_action :authorize_create_cluster!, only: [:new, :create]
before_action :verify_billing, only: [:create]
before_action :authorize_google_api, except: :login
helper_method :token_in_session
def login
begin
......@@ -37,21 +36,6 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
private
def verify_billing
case google_project_billing_status
when nil
flash.now[:alert] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.')
when false
flash.now[: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
@cluster = ::Clusters::Cluster.new(create_params)
render :new
end
def create_params
params.require(:cluster).permit(
:enabled,
......@@ -75,18 +59,8 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
end
end
def authorize_google_project_billing
redis_token_key = CheckGcpProjectBillingWorker.store_session_token(token_in_session)
CheckGcpProjectBillingWorker.perform_async(redis_token_key)
end
def google_project_billing_status
CheckGcpProjectBillingWorker.get_billing_state(token_in_session)
end
def token_in_session
@token_in_session ||=
session[GoogleApi::CloudPlatform::Client.session_key_for_token]
session[GoogleApi::CloudPlatform::Client.session_key_for_token]
end
def expires_at_in_session
......
......@@ -71,19 +71,6 @@ class Projects::ClustersController < Projects::ApplicationController
.present(current_user: current_user)
end
def create_params
params.require(:cluster).permit(
:enabled,
:name,
:provider_type,
provider_gcp_attributes: [
:gcp_project_id,
:zone,
:num_nodes,
:machine_type
])
end
def update_params
if cluster.managed?
params.require(:cluster).permit(
......
......@@ -7,6 +7,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize]
before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics]
before_action :verify_api_request!, only: :terminal_websocket_authorize
before_action :expire_etag_cache, only: [:index]
def index
@environments = project.environments
......@@ -148,6 +149,15 @@ class Projects::EnvironmentsController < Projects::ApplicationController
Gitlab::Workhorse.verify_api_request!(request.headers)
end
def expire_etag_cache
return if request.format.json?
# this forces to reload json content
Gitlab::EtagCaching::Store.new.tap do |store|
store.touch(project_environments_path(project, format: :json))
end
end
def environment_params
params.require(:environment).permit(:name, :external_url)
end
......
......@@ -14,6 +14,8 @@ class Projects::WikisController < Projects::ApplicationController
def show
@page = @project_wiki.find_page(params[:id], params[:version_id])
view_param = @project_wiki.empty? ? params[:view] : 'create'
if @page
render 'show'
elsif file = @project_wiki.find_file(params[:id], params[:version_id])
......@@ -26,12 +28,12 @@ class Projects::WikisController < Projects::ApplicationController
disposition: 'inline',
filename: file.name
)
else
return render('empty') unless can?(current_user, :create_wiki, @project)
elsif can?(current_user, :create_wiki, @project) && view_param == 'create'
@page = build_page(title: params[:id])
render 'edit'
else
render 'empty'
end
end
......
......@@ -39,25 +39,15 @@ class GroupProjectsFinder < ProjectsFinder
end
def collection_with_user
if group.users.include?(current_user)
if only_shared?
[shared_projects]
elsif only_owned?
[owned_projects]
else
[shared_projects, owned_projects]
end
if only_shared?
[shared_projects.public_or_visible_to_user(current_user)]
elsif only_owned?
[owned_projects.public_or_visible_to_user(current_user)]
else
if only_shared?
[shared_projects.public_or_visible_to_user(current_user)]
elsif only_owned?
[owned_projects.public_or_visible_to_user(current_user)]
else
[
owned_projects.public_or_visible_to_user(current_user),
shared_projects.public_or_visible_to_user(current_user)
]
end
[
owned_projects.public_or_visible_to_user(current_user),
shared_projects.public_or_visible_to_user(current_user)
]
end
end
......
......@@ -204,7 +204,7 @@ module ApplicationSettingsHelper
:pages_domain_verification_enabled,
:password_authentication_enabled_for_web,
:password_authentication_enabled_for_git,
:performance_bar_allowed_group_id,
:performance_bar_allowed_group_path,
:performance_bar_enabled,
:plantuml_enabled,
:plantuml_url,
......
module CountHelper
def approximate_count_with_delimiters(model)
number_with_delimiter(Gitlab::Database::Count.approximate_count(model))
def approximate_count_with_delimiters(count_data, model)
count = count_data[model]
raise "Missing model #{model} from count data" unless count
number_with_delimiter(count)
end
end
......@@ -11,6 +11,7 @@ module NavHelper
class_name = page_gutter_class
class_name << 'page-with-contextual-sidebar' if defined?(@left_sidebar) && @left_sidebar
class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @left_sidebar
class_name -= ['right-sidebar-expanded'] if defined?(@right_sidebar) && !@right_sidebar
class_name
end
......
......@@ -257,6 +257,9 @@ module ProjectsHelper
if project.builds_enabled? && can?(current_user, :read_pipeline, project)
nav_tabs << :pipelines
end
if can?(current_user, :read_environment, project) || can?(current_user, :read_cluster, project)
nav_tabs << :operations
end
......
......@@ -230,6 +230,7 @@ class ApplicationSetting < ActiveRecord::Base
after_commit do
reset_memoized_terms
end
after_commit :expire_performance_bar_allowed_user_ids_cache, if: -> { previous_changes.key?('performance_bar_allowed_group_id') }
def self.defaults
{
......@@ -386,31 +387,6 @@ class ApplicationSetting < ActiveRecord::Base
super(levels.map { |level| Gitlab::VisibilityLevel.level_value(level) })
end
def performance_bar_allowed_group_id=(group_full_path)
group_full_path = nil if group_full_path.blank?
if group_full_path.nil?
if group_full_path != performance_bar_allowed_group_id
super(group_full_path)
Gitlab::PerformanceBar.expire_allowed_user_ids_cache
end
return
end
group = Group.find_by_full_path(group_full_path)
if group
if group.id != performance_bar_allowed_group_id
super(group.id)
Gitlab::PerformanceBar.expire_allowed_user_ids_cache
end
else
super(nil)
Gitlab::PerformanceBar.expire_allowed_user_ids_cache
end
end
def performance_bar_allowed_group
Group.find_by_id(performance_bar_allowed_group_id)
end
......@@ -420,15 +396,6 @@ class ApplicationSetting < ActiveRecord::Base
performance_bar_allowed_group_id.present?
end
# - If `enable` is true, we early return since the actual attribute that holds
# the enabling/disabling is `performance_bar_allowed_group_id`
# - If `enable` is false, we set `performance_bar_allowed_group_id` to `nil`
def performance_bar_enabled=(enable)
return if Gitlab::Utils.to_boolean(enable)
self.performance_bar_allowed_group_id = nil
end
# Choose one of the available repository storage options. Currently all have
# equal weighting.
def pick_repository_storage
......@@ -506,4 +473,8 @@ class ApplicationSetting < ActiveRecord::Base
errors.add(:terms, "You need to set terms to be enforced") unless terms.present?
end
def expire_performance_bar_allowed_user_ids_cache
Gitlab::PerformanceBar.expire_allowed_user_ids_cache
end
end
# Provides a way to work around Rails issue where dependent objects are all
# loaded into memory before destroyed: https://github.com/rails/rails/issues/22510.
#
# This concern allows an ActiveRecord module to destroy all its dependent
# associations in batches. The idea is borrowed from https://github.com/thisismydesign/batch_dependent_associations.
#
# The differences here with that gem:
#
# 1. We allow excluding certain associations.
# 2. We don't need to support delete_all since we can use the EachBatch concern.
module BatchDestroyDependentAssociations
extend ActiveSupport::Concern
DEPENDENT_ASSOCIATIONS_BATCH_SIZE = 1000
def dependent_associations_to_destroy
self.class.reflect_on_all_associations(:has_many).select { |assoc| assoc.options[:dependent] == :destroy }
end
def destroy_dependent_associations_in_batches(exclude: [])
dependent_associations_to_destroy.each do |association|
next if exclude.include?(association.name)
# rubocop:disable GitlabSecurity/PublicSend
public_send(association.name).find_each(batch_size: DEPENDENT_ASSOCIATIONS_BATCH_SIZE, &:destroy)
end
end
end
module DiffFile
extend ActiveSupport::Concern
def to_hash
keys = Gitlab::Git::Diff::SERIALIZE_KEYS - [:diff]
as_json(only: keys).merge(diff: diff).with_indifferent_access
end
end
......@@ -3,6 +3,7 @@
# A note of this type can be resolvable.
class DiffNote < Note
include NoteOnDiff
include Gitlab::Utils::StrongMemoize
NOTEABLE_TYPES = %w(MergeRequest Commit).freeze
......@@ -12,7 +13,6 @@ class DiffNote < Note
validates :original_position, presence: true
validates :position, presence: true
validates :diff_line, presence: true, if: :on_text?
validates :line_code, presence: true, line_code: true, if: :on_text?
validates :noteable_type, inclusion: { in: NOTEABLE_TYPES }
validate :positions_complete
......@@ -23,6 +23,7 @@ class DiffNote < Note
before_validation :update_position, on: :create, if: :on_text?
before_validation :set_line_code, if: :on_text?
after_save :keep_around_commits
after_commit :create_diff_file, on: :create
def discussion_class(*)
DiffDiscussion
......@@ -53,21 +54,25 @@ class DiffNote < Note
position.position_type == "image"
end
def create_diff_file
return unless should_create_diff_file?
diff_file = fetch_diff_file
diff_line = diff_file.line_for_position(self.original_position)
creation_params = diff_file.diff.to_hash
.except(:too_large)
.merge(diff: diff_file.diff_hunk(diff_line))
create_note_diff_file(creation_params)
end
def diff_file
@diff_file ||=
begin
if created_at_diff?(noteable.diff_refs)
# We're able to use the already persisted diffs (Postgres) if we're
# presenting a "current version" of the MR discussion diff.
# So no need to make an extra Gitaly diff request for it.
# As an extra benefit, the returned `diff_file` already
# has `highlighted_diff_lines` data set from Redis on
# `Diff::FileCollection::MergeRequestDiff`.
noteable.diffs(paths: original_position.paths, expanded: true).diff_files.first
else
original_position.diff_file(self.project.repository)
end
end
strong_memoize(:diff_file) do
enqueue_diff_file_creation_job if should_create_diff_file?
fetch_diff_file
end
end
def diff_line
......@@ -98,6 +103,38 @@ class DiffNote < Note
private
def enqueue_diff_file_creation_job
# Avoid enqueuing multiple file creation jobs at once for a note (i.e.
# parallel calls to `DiffNote#diff_file`).
lease = Gitlab::ExclusiveLease.new("note_diff_file_creation:#{id}", timeout: 1.hour.to_i)
return unless lease.try_obtain
CreateNoteDiffFileWorker.perform_async(id)
end
def should_create_diff_file?
on_text? && note_diff_file.nil? && self == discussion.first_note
end
def fetch_diff_file
if note_diff_file
diff = Gitlab::Git::Diff.new(note_diff_file.to_hash)
Gitlab::Diff::File.new(diff,
repository: project.repository,
diff_refs: original_position.diff_refs)
elsif created_at_diff?(noteable.diff_refs)
# We're able to use the already persisted diffs (Postgres) if we're
# presenting a "current version" of the MR discussion diff.
# So no need to make an extra Gitaly diff request for it.
# As an extra benefit, the returned `diff_file` already
# has `highlighted_diff_lines` data set from Redis on
# `Diff::FileCollection::MergeRequestDiff`.
noteable.diffs(paths: original_position.paths, expanded: true).diff_files.first
else
original_position.diff_file(self.project.repository)
end
end
def supported?
for_commit? || self.noteable.has_complete_diff_refs?
end
......
......@@ -40,6 +40,7 @@ class Event < ActiveRecord::Base
).freeze
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
REPOSITORY_UPDATED_AT_INTERVAL = 5.minutes
delegate :name, :email, :public_email, :username, to: :author, prefix: true, allow_nil: true
delegate :title, to: :issue, prefix: true, allow_nil: true
......@@ -391,6 +392,7 @@ class Event < ActiveRecord::Base
def set_last_repository_updated_at
Project.unscoped.where(id: project_id)
.where("last_repository_updated_at < ? OR last_repository_updated_at IS NULL", REPOSITORY_UPDATED_AT_INTERVAL.ago)
.update_all(last_repository_updated_at: created_at)
end
......
......@@ -24,12 +24,9 @@ class InternalId < ActiveRecord::Base
#
# The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL).
# As such, the increment is atomic and safe to be called concurrently.
#
# If a `maximum_iid` is passed in, this overrides the incremented value if it's
# greater than that. This can be used to correct the increment value if necessary.
def increment_and_save!(maximum_iid)
def increment_and_save!
lock!
self.last_value = [(last_value || 0) + 1, (maximum_iid || 0) + 1].max
self.last_value = (last_value || 0) + 1
save!
last_value
end
......@@ -93,16 +90,7 @@ class InternalId < ActiveRecord::Base
# and increment its last value
#
# Note this will acquire a ROW SHARE lock on the InternalId record
# Note we always calculate the maximum iid present here and
# pass it in to correct the InternalId entry if it's last_value is off.
#
# This can happen in a transition phase where both `AtomicInternalId` and
# `NonatomicInternalId` code runs (e.g. during a deploy).
#
# This is subject to be cleaned up with the 10.8 release:
# https://gitlab.com/gitlab-org/gitlab-ce/issues/45389.
(lookup || create_record).increment_and_save!(maximum_iid)
(lookup || create_record).increment_and_save!
end
end
......@@ -128,15 +116,11 @@ class InternalId < ActiveRecord::Base
InternalId.create!(
**scope,
usage: usage_value,
last_value: maximum_iid
last_value: init.call(subject) || 0
)
end
rescue ActiveRecord::RecordNotUnique
lookup
end
def maximum_iid
@maximum_iid ||= init.call(subject) || 0
end
end
end
class MergeRequestDiffFile < ActiveRecord::Base
include Gitlab::EncodingHelper
include DiffFile
belongs_to :merge_request_diff
......@@ -12,10 +13,4 @@ class MergeRequestDiffFile < ActiveRecord::Base
def diff
binary? ? super.unpack('m0').first : super
end
def to_hash
keys = Gitlab::Git::Diff::SERIALIZE_KEYS - [:diff]
as_json(only: keys).merge(diff: diff).with_indifferent_access
end
end
......@@ -63,6 +63,7 @@ class Note < ActiveRecord::Base
has_many :todos
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :system_note_metadata
has_one :note_diff_file, inverse_of: :diff_note, foreign_key: :diff_note_id
delegate :gfm_reference, :local_reference, to: :noteable
delegate :name, to: :project, prefix: true
......@@ -100,7 +101,8 @@ class Note < ActiveRecord::Base
scope :inc_author_project, -> { includes(:project, :author) }
scope :inc_author, -> { includes(:author) }
scope :inc_relations_for_view, -> do
includes(:project, :author, :updated_by, :resolved_by, :award_emoji, :system_note_metadata)
includes(:project, :author, :updated_by, :resolved_by, :award_emoji,
:system_note_metadata, :note_diff_file)
end
scope :diff_notes, -> { where(type: %w(LegacyDiffNote DiffNote)) }
......
class NoteDiffFile < ActiveRecord::Base
include DiffFile
belongs_to :diff_note, inverse_of: :note_diff_file
validates :diff_note, presence: true
end
......@@ -24,6 +24,7 @@ class Project < ActiveRecord::Base
include ChronicDurationAttribute
include FastDestroyAll::Helpers
include WithUploads
include BatchDestroyDependentAssociations
extend Gitlab::ConfigHelper
......
......@@ -3,6 +3,10 @@ module ApplicationSettings
def execute
update_terms(@params.delete(:terms))
if params.key?(:performance_bar_allowed_group_path)
params[:performance_bar_allowed_group_id] = performance_bar_allowed_group_id
end
@application_setting.update(@params)
end
......@@ -18,5 +22,13 @@ module ApplicationSettings
ApplicationSetting::Term.create(terms: terms)
@application_setting.reset_memoized_terms
end
def performance_bar_allowed_group_id
performance_bar_enabled = !params.key?(:performance_bar_enabled) || params.delete(:performance_bar_enabled)
group_full_path = params.delete(:performance_bar_allowed_group_path)
return nil unless Gitlab::Utils.to_boolean(performance_bar_enabled)
Group.find_by_full_path(group_full_path)&.id if group_full_path.present?
end
end
end
class CheckGcpProjectBillingService
def execute(token)
client = GoogleApi::CloudPlatform::Client.new(token, nil)
client.projects_list.select do |project|
begin
client.projects_get_billing_info(project.project_id).billing_enabled
rescue
end
end
end
end
......@@ -137,7 +137,13 @@ module Projects
trash_repositories!
project.team.truncate
# Rails attempts to load all related records into memory before
# destroying: https://github.com/rails/rails/issues/22510
# This ensures we delete records in batches.
#
# Exclude container repositories because its before_destroy would be
# called multiple times, and it doesn't destroy any database records.
project.destroy_dependent_associations_in_batches(exclude: [:container_repositories])
project.destroy!
end
end
......
......@@ -11,7 +11,7 @@ module ObjectStorage
ObjectStorageUnavailable = Class.new(StandardError)
DIRECT_UPLOAD_TIMEOUT = 4.hours
TMP_UPLOAD_PATH = 'tmp/upload'.freeze
TMP_UPLOAD_PATH = 'tmp/uploads'.freeze
module Store
LOCAL = 1
......
......@@ -9,8 +9,8 @@
= f.check_box :performance_bar_enabled
Enable the Performance Bar
.form-group.row
= f.label :performance_bar_allowed_group_id, 'Allowed group', class: 'col-form-label col-sm-2'
= f.label :performance_bar_allowed_group_path, 'Allowed group', class: 'col-form-label col-sm-2'
.col-sm-10
= f.text_field :performance_bar_allowed_group_id, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
= f.text_field :performance_bar_allowed_group_path, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
= f.submit 'Save changes', class: "btn btn-success"
......@@ -9,7 +9,7 @@
= f.label :mirror_available do
= f.check_box :mirror_available
Allow mirrors to be setup for projects
%span.help-block
%span.form-text.text-muted
If disabled, only admins will be able to setup mirrors in projects.
= link_to icon('question-circle'), help_page_path('workflow/repository_mirroring')
......
......@@ -23,7 +23,7 @@
must be used to authenticate.
- if omniauth_enabled? && button_based_providers.any?
.form-group.row
= f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'control-label col-sm-2'
= f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'col-form-label col-sm-2'
= hidden_field_tag 'application_setting[enabled_oauth_sign_in_sources][]'
.col-sm-10
.btn-group{ data: { toggle: 'buttons' } }
......
......@@ -8,7 +8,7 @@
= f.label :enforce_terms do
= f.check_box :enforce_terms
= _("Require all users to accept Terms of Service when they access GitLab.")
.help-block
.form-text.text-muted
= _("When enabled, users cannot use GitLab until the terms have been accepted.")
.form-group
.col-sm-12
......@@ -16,7 +16,7 @@
= _("Terms of Service Agreement")
.col-sm-12
= f.text_area :terms, class: 'form-control', rows: 8
.help-block
.form-text.text-muted
= _("Markdown enabled")
= f.submit _("Save changes"), class: "btn btn-success"
......@@ -27,7 +27,7 @@
.form-check
= level
%span.form-text.text-muted#restricted-visibility-help
Selected levels cannot be used by non-admin users for projects or snippets.
Selected levels cannot be used by non-admin users for groups, projects or snippets.
If the public level is restricted, user profiles are only visible to logged in users.
.form-group.row
= f.label :import_sources, class: 'col-form-label col-sm-2'
......
......@@ -12,7 +12,7 @@
= link_to admin_projects_path do
%h3.text-center
Projects:
= approximate_count_with_delimiters(Project)
= approximate_count_with_delimiters(@counts, Project)
%hr
= link_to('New project', new_project_path, class: "btn btn-new")
.col-sm-4
......@@ -21,7 +21,7 @@
= link_to admin_users_path do
%h3.text-center
Users:
= approximate_count_with_delimiters(User)
= approximate_count_with_delimiters(@counts, User)
= render_if_exists 'users_statistics'
%hr
= link_to 'New user', new_admin_user_path, class: "btn btn-new"
......@@ -31,7 +31,7 @@
= link_to admin_groups_path do
%h3.text-center
Groups:
= approximate_count_with_delimiters(Group)
= approximate_count_with_delimiters(@counts, Group)
%hr
= link_to 'New group', new_admin_group_path, class: "btn btn-new"
.row
......@@ -42,31 +42,31 @@
%p
Forks
%span.light.float-right
= approximate_count_with_delimiters(ForkedProjectLink)
= approximate_count_with_delimiters(@counts, ForkedProjectLink)
%p
Issues
%span.light.float-right
= approximate_count_with_delimiters(Issue)
= approximate_count_with_delimiters(@counts, Issue)
%p
Merge Requests
%span.light.float-right
= approximate_count_with_delimiters(MergeRequest)
= approximate_count_with_delimiters(@counts, MergeRequest)
%p
Notes
%span.light.float-right
= approximate_count_with_delimiters(Note)
= approximate_count_with_delimiters(@counts, Note)
%p
Snippets
%span.light.float-right
= approximate_count_with_delimiters(Snippet)
= approximate_count_with_delimiters(@counts, Snippet)
%p
SSH Keys
%span.light.float-right
= approximate_count_with_delimiters(Key)
= approximate_count_with_delimiters(@counts, Key)
%p
Milestones
%span.light.float-right
= approximate_count_with_delimiters(Milestone)
= approximate_count_with_delimiters(@counts, Milestone)
%p
Active Users
%span.light.float-right
......
......@@ -13,7 +13,7 @@
.card
.card-header
Group info:
%ul.well-list
%ul.content-list
%li
.avatar-container.s60
= group_icon(@group, class: "avatar s60")
......@@ -64,7 +64,7 @@
Projects
%span.badge.badge-pill
#{@group.projects.count}
%ul.well-list
%ul.content-list
- @projects.each do |project|
%li
%strong
......@@ -82,7 +82,7 @@
Projects shared with #{@group.name}
%span.badge.badge-pill
#{@group.shared_projects.count}
%ul.well-list
%ul.content-list
- @group.shared_projects.sort_by(&:name).each do |project|
%li
%strong
......@@ -118,7 +118,7 @@
%span.badge.badge-pill= @group.members.size
.float-right
= link_to icon('pencil-square-o', text: 'Manage access'), polymorphic_url([@group, :members]), class: "btn btn-sm"
%ul.well-list.group-users-list.content-list.members-list
%ul.content-list.group-users-list.content-list.members-list
= render partial: 'shared/members/member', collection: @members, as: :member, locals: { show_controls: false }
.card-footer
= paginate @members, param_name: 'members_page', theme: 'gitlab'
......@@ -22,7 +22,7 @@
.card
.card-header
Project info:
%ul.well-list
%ul.content-list
%li
%span.light Name:
%strong
......@@ -166,7 +166,7 @@
.float-right
= link_to admin_group_path(@group), class: 'btn btn-sm' do
= icon('pencil-square-o', text: 'Manage access')
%ul.well-list.content-list.members-list
%ul.content-list.members-list
= render partial: 'shared/members/member', collection: @group_members, as: :member, locals: { show_controls: false }
.card-footer
= paginate @group_members, param_name: 'group_members_page', theme: 'gitlab'
......@@ -180,7 +180,7 @@
%span.badge.badge-pill= @project.users.size
.float-right
= link_to icon('pencil-square-o', text: 'Manage access'), polymorphic_url([@project, :members]), class: "btn btn-sm"
%ul.well-list.project_members.content-list.members-list
%ul.content-list.project_members.members-list
= render partial: 'shared/members/member', collection: @project_members, as: :member, locals: { show_controls: false }
.card-footer
= paginate @project_members, param_name: 'project_members_page', theme: 'gitlab'
.card
.card-header
Profile
%ul.well-list
%ul.content-list
%li
%span.light Member since
%strong= user.created_at.to_s(:medium)
......
......@@ -4,7 +4,7 @@
- if @user.groups.any?
.card
.card-header Group projects
%ul.card-body-list
%ul.hover-list
- @user.group_members.includes(:source).each do |group_member|
- group = group_member.group
%li.group_member
......@@ -28,7 +28,7 @@
.col-md-6
.card
.card-header Joined projects (#{@joined_projects.count})
%ul.card-body-list
%ul.hover-list
- @joined_projects.sort_by(&:full_name).each do |project|
- member = project.team.find_member(@user.id)
%li.project_member
......
......@@ -8,7 +8,7 @@
.card
.card-header
= @user.name
%ul.well-list
%ul.content-list
%li
= image_tag avatar_icon_for_user(@user, 60), class: "avatar s60"
%li
......@@ -21,7 +21,7 @@
.card
.card-header
Account:
%ul.well-list
%ul.content-list
%li
%span.light Name:
%strong= @user.name
......
......@@ -14,7 +14,7 @@
- if event.push_with_commits?
.event-body
%ul.well-list.event_commits
%ul.content-list.event_commits
= render "events/commit", project: project, event: event
- create_mr = event.new_ref? && create_mr_button?(project.default_branch, event.ref_name, project) && event.authored_by?(current_user)
......
- breadcrumb_title "General Settings"
- @content_class = "limit-container-width" unless fluid_layout
.card.prepend-top-default
.card-header
Group settings
.card-body
= form_for @group, html: { multipart: true, class: "gl-show-field-errors" }, authenticity_token: true do |f|
= form_errors(@group)
= render 'shared/group_form', f: f
.form-group.row
.offset-sm-2.col-sm-10
.avatar-container.s160
= group_icon(@group, alt: '', class: 'avatar group-avatar s160')
%p.light
- if @group.avatar?
You can change the group avatar here
- else
You can upload a group avatar here
= render 'shared/choose_group_avatar_button', f: f
- if @group.avatar?
%hr
= link_to _('Remove avatar'), group_avatar_path(@group.to_param), data: { confirm: _("Avatar will be removed. Are you sure?")}, method: :delete, class: "btn btn-danger btn-inverted"
= render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
.form-group.row
.offset-sm-2.col-sm-10
= render 'shared/allow_request_access', form: f
.form-group.row
%label.col-form-label.col-sm-2
= s_("GroupSettings|Share with group lock")
.col-sm-10
.form-check
= f.label :share_with_group_lock do
= f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group)
%strong
- group_link = link_to @group.name, group_path(@group)
= s_("GroupSettings|Prevent sharing a project within %{group} with other groups").html_safe % { group: group_link }
%br
%span.descr= share_with_group_lock_help_text(@group)
= render 'group_admin_settings', f: f
.form-actions
= f.submit 'Save group', class: "btn btn-save"
.card.bg-danger
.card-header Remove group
.card-body
= form_tag(@group, method: :delete) do
%p
Removing group will cause all child projects and resources to be removed.
%br
%strong Removed group can not be restored!
.form-actions
= button_to 'Remove group', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_group_message(@group) }
- if supports_nested_groups?
.card.bg-warning
.card-header Transfer group
.card-body
= form_for @group, url: transfer_group_path(@group), method: :put do |f|
.form-group
= dropdown_tag('Select parent group', options: { toggle_class: 'js-groups-dropdown', title: 'Parent Group', filter: true, dropdown_class: 'dropdown-open-top dropdown-group-transfer', placeholder: "Search groups", data: { data: parent_group_options(@group) } })
= hidden_field_tag 'new_parent_group_id'
%ul
%li Be careful. Changing a group's parent can have unintended #{link_to 'side effects', 'https://docs.gitlab.com/ce/user/project/index.html#redirects-when-changing-repository-paths', target: 'blank'}.
%li You can only transfer the group to a group you manage.
%li You will need to update your local repositories to point to the new location.
%li If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility.
= f.submit 'Transfer group', class: "btn btn-warning"
- expanded = Rails.env.test?
%section.settings.gs-general.no-animate#js-general-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('General')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
= _('Update your group name, description, avatar, and other general settings.')
.settings-content
= render 'groups/settings/general'
%section.settings.gs-permissions.no-animate#js-permissions-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Permissions')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
= _('Enable or disable certain group features and choose access levels.')
.settings-content
= render 'groups/settings/permissions'
%section.settings.gs-advanced.no-animate#js-advanced-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Advanced')
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
= _('Perform advanced options such as changing path, transferring, or removing the group.')
.settings-content
= render 'groups/settings/advanced'
= render 'shared/confirm_modal', phrase: @group.path
......@@ -8,7 +8,7 @@
.controls
= link_to new_project_path(namespace_id: @group.id), class: "btn btn-sm btn-success" do
New project
%ul.well-list
%ul.content-list
- @projects.each do |project|
%li
.list-item-name
......
......@@ -8,13 +8,13 @@
= link_to edit_group_runner_path(@group, runner) do
= icon('edit')
.pull-right
.float-right
- if runner.active?
= link_to _('Pause'), pause_group_runner_path(@group, runner), method: :post, class: 'btn btn-sm btn-danger', data: { confirm: _("Are you sure?") }
- else
= link_to _('Resume'), resume_group_runner_path(@group, runner), method: :post, class: 'btn btn-success btn-sm'
= link_to _('Remove Runner'), group_runner_path(@group, runner), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm'
.pull-right
.float-right
%small.light
\##{runner.id}
- if runner.description.present?
......
.sub-section
%h4.warning-title Change group path
= form_for @group, html: { multipart: true, class: 'gl-show-field-errors' }, authenticity_token: true do |f|
= form_errors(@group)
.form-group
%p
Changing group path can have unintended side effects.
= succeed '.' do
= link_to 'Learn more', help_page_path('user/group/index', anchor: 'changing-a-groups-path'), target: '_blank'
.input-group.gl-field-error-anchor
.group-root-path.input-group-prepend.has-tooltip{ title: group_path(@group), :'data-placement' => 'bottom' }
.input-group-text
%span>= root_url
- if parent
%strong= parent.full_path + '/'
= f.hidden_field :parent_id
= f.text_field :path, placeholder: 'open-source', class: 'form-control',
autofocus: local_assigns[:autofocus] || false, required: true,
pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS,
title: 'Please choose a group path with no special characters.',
"data-bind-in" => "#{'create_chat_team' if Gitlab.config.mattermost.enabled}"
= f.submit 'Change group path', class: 'btn btn-warning'
.sub-section
%h4.danger-title Remove group
= form_tag(@group, method: :delete) do
%p
Removing group will cause all child projects and resources to be removed.
%br
%strong Removed group can not be restored!
= button_to 'Remove group', '#', class: 'btn btn-remove js-confirm-danger', data: { 'confirm-danger-message' => remove_group_message(@group) }
- if supports_nested_groups?
.sub-section
%h4.warning-title Transfer group
= form_for @group, url: transfer_group_path(@group), method: :put do |f|
.form-group
= dropdown_tag('Select parent group', options: { toggle_class: 'js-groups-dropdown', title: 'Parent Group', filter: true, dropdown_class: 'dropdown-open-top dropdown-group-transfer', placeholder: 'Search groups', data: { data: parent_group_options(@group) } })
= hidden_field_tag 'new_parent_group_id'
%ul
%li Be careful. Changing a group's parent can have unintended #{link_to 'side effects', 'https://docs.gitlab.com/ce/user/project/index.html#redirects-when-changing-repository-paths', target: 'blank'}.
%li You can only transfer the group to a group you manage.
%li You will need to update your local repositories to point to the new location.
%li If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility.
= f.submit 'Transfer group', class: 'btn btn-warning'
= form_for @group, html: { multipart: true, class: 'gl-show-field-errors' }, authenticity_token: true do |f|
= form_errors(@group)
%fieldset
.row
.form-group.col-md-9
= f.label :name, class: 'label-light' do
Group name
= f.text_field :name, class: 'form-control'
.form-group.col-md-3
= f.label :id, class: 'label-light' do
Group ID
= f.text_field :id, class: 'form-control', readonly: true
.form-group
= f.label :description, class: 'label-light' do
Group description
%span.light (optional)
= f.text_area :description, class: 'form-control', rows: 3, maxlength: 250
= render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group
.form-group.row
.col-sm-12
.avatar-container.s160
= group_icon(@group, alt: '', class: 'avatar group-avatar s160')
%p.light
- if @group.avatar?
You can change the group avatar here
- else
You can upload a group avatar here
= render 'shared/choose_group_avatar_button', f: f
- if @group.avatar?
%hr
= link_to _('Remove avatar'), group_avatar_path(@group.to_param), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'btn btn-danger btn-inverted'
= f.submit 'Save group', class: 'btn btn-success'
= form_for @group, html: { multipart: true, class: 'gl-show-field-errors' }, authenticity_token: true do |f|
= form_errors(@group)
%fieldset
= render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
.form-group.row
.offset-sm-2.col-sm-10
= render 'shared/allow_request_access', form: f
.form-group.row
%label.col-form-label.col-sm-2
= s_('GroupSettings|Share with group lock')
.col-sm-10
.form-check
= f.label :share_with_group_lock do
= f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group)
%strong
- group_link = link_to @group.name, group_path(@group)
= s_('GroupSettings|Prevent sharing a project within %{group} with other groups').html_safe % { group: group_link }
%br
%span.descr= share_with_group_lock_help_text(@group)
= render 'groups/group_admin_settings', f: f
= render_if_exists 'groups/member_lock_setting', f: f, group: @group
= f.submit 'Save group', class: 'btn btn-success'
......@@ -6,7 +6,7 @@
%section.settings#secret-variables.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
= _('Secret variables')
= _('Variables')
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer'
%button.btn.btn-default.js-settings-toggle{ type: "button" }
= expanded ? _('Collapse') : _('Expand')
......
......@@ -36,7 +36,7 @@
.card
.card-header
Quick help
%ul.well-list
%ul.content-list
%li= link_to 'See our website for getting help', support_url
%li
%button.btn-blank.btn-link.js-trigger-search-bar{ type: 'button' }
......
......@@ -116,9 +116,9 @@
.lead
List with hover effect
%code .well-list
%code .hover-list
.example
%ul.well-list
%ul.hover-list
%li
One item
%li
......@@ -131,7 +131,7 @@
.example
.card
.card-header Your list
%ul.well-list
%ul.content-list
%li
One item
%li
......
%h5.prepend-top-0
History of authentications
%ul.well-list
%ul.content-list
- events.each do |event|
%li
%span.description
......
- is_current_session = active_session.current?(session)
%li.list-group-item
.pull-left.append-right-10{ data: { toggle: 'tooltip' }, title: active_session.human_device_type }
.float-left.append-right-10{ data: { toggle: 'tooltip' }, title: active_session.human_device_type }
= active_session_device_type_icon(active_session)
.description.pull-left
.description.float-left
%div
%strong= active_session.ip_address
- if is_current_session
......@@ -25,7 +25,7 @@
= l(active_session.created_at, format: :short)
- unless is_current_session
.pull-right
.float-right
= link_to profile_active_session_path(active_session.session_id), data: { confirm: 'Are you sure? The device will be signed out of GitLab.' }, method: :delete, class: "btn btn-danger prepend-left-10" do
%span.sr-only Revoke
Revoke
......@@ -29,7 +29,7 @@
Your Public Email will be displayed on your public profile.
%li
All email addresses will be used to identify your commits.
%ul.well-list
%ul.content-list
%li
= render partial: 'shared/email_with_badge', locals: { email: @primary_email, verified: current_user.confirmed? }
%span.float-right
......
- is_admin = local_assigns.fetch(:admin, false)
- if @gpg_keys.any?
%ul.well-list
%ul.content-list
= render partial: 'profiles/gpg_keys/key', collection: @gpg_keys, locals: { is_admin: is_admin }
- else
%p.settings-message.text-center
......
......@@ -4,7 +4,7 @@
.card
.card-header
SSH Key
%ul.well-list
%ul.content-list
%li
%span.light Title:
%strong= @key.title
......
- is_admin = local_assigns.fetch(:admin, false)
- if @keys.any?
%ul.well-list
%ul.content-list
= render partial: 'profiles/keys/key', collection: @keys, locals: { is_admin: is_admin }
- else
%p.settings-message.text-center
......
......@@ -28,7 +28,7 @@
= s_('Branches|Cant find HEAD commit for this branch')
- if branch.name != @repository.root_ref
.divergence-graph.d-none.d-sm-block{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind),
.divergence-graph.d-none.d-md-block{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind),
default_branch: @repository.root_ref,
number_commits_ahead: diverging_count_label(number_commits_ahead) } }
.graph-side
......@@ -39,7 +39,7 @@
.bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" }
%span.count.count-ahead= diverging_count_label(number_commits_ahead)
.controls.d-none.d-sm-block<
.controls.d-none.d-md-block<
- if merge_project && create_mr_button?(@repository.root_ref, branch.name)
= link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-default' do
= _('Merge request')
......
= javascript_include_tag 'https://apis.google.com/js/api.js'
%p
- link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration.').html_safe % { link_to_help_page: link_to_help_page}
= form_for @cluster, html: { class: 'prepend-top-20' }, url: gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
= form_for @cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
= form_errors(@cluster)
.form-group
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name')
......@@ -14,13 +16,25 @@
= field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field|
.form-group
= provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID')
= link_to(s_('ClusterIntegration|See your projects'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer')
= provider_gcp_field.text_field :gcp_project_id, class: 'form-control', placeholder: s_('ClusterIntegration|Project ID')
.js-gcp-project-id-dropdown-entry-point{ data: { docsUrl: 'https://console.cloud.google.com/home/dashboard' } }
= provider_gcp_field.hidden_field :gcp_project_id
.dropdown
%button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', disabled: true }
%span.dropdown-toggle-text
= _('Select project')
= icon('chevron-down')
%span.form-text.text-muted &nbsp;
.form-group
= provider_gcp_field.label :zone, s_('ClusterIntegration|Zone')
= link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer')
= provider_gcp_field.text_field :zone, class: 'form-control', placeholder: 'us-central1-a'
.js-gcp-zone-dropdown-entry-point
= provider_gcp_field.hidden_field :zone
.dropdown
%button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', disabled: true }
%span.dropdown-toggle-text
= _('Select project to choose zone')
= icon('chevron-down')
.form-group
= provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes')
......@@ -28,8 +42,13 @@
.form-group
= provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type')
= link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer')
= provider_gcp_field.text_field :machine_type, class: 'form-control', placeholder: 'n1-standard-4'
.js-gcp-machine-type-dropdown-entry-point
= provider_gcp_field.hidden_field :machine_type
.dropdown
%button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', disabled: true }
%span.dropdown-toggle-text
= _('Select project and zone to choose machine type')
= icon('chevron-down')
.form-group
= field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'btn btn-success'
= field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true
......@@ -15,7 +15,7 @@
%h5.prepend-top-default
Webhooks (#{@hooks.count})
- if @hooks.any?
%ul.well-list
%ul.content-list
- @hooks.each do |hook|
= render 'project_hook', hook: hook
- else
......
......@@ -18,7 +18,7 @@
%span= time_ago_with_tooltip @build.artifacts_expire_at
- if @build.artifacts?
.btn-group.btn-group.d-flex{ role: :group }
.btn-group.d-flex{ role: :group }
- if @build.has_expiring_artifacts? && can?(current_user, :update_build, @build)
= link_to keep_project_job_artifacts_path(@project, @build), class: 'btn btn-sm btn-default', method: :post do
Keep
......@@ -42,7 +42,7 @@
- if @build.trigger_variables.any?
%p
%button.btn.group.btn-group.js-reveal-variables Reveal Variables
%button.btn.group.js-reveal-variables Reveal Variables
%dl.js-build-variables.trigger-build-variables.hide
- @build.trigger_variables.each do |trigger_variable|
......
......@@ -30,7 +30,7 @@
#{h(@remote_mirror.last_error.strip)}
= f.fields_for :remote_mirrors, @remote_mirror do |rm_form|
.form-group
= rm_form.check_box :enabled, class: "pull-left"
= rm_form.check_box :enabled, class: "float-left"
.prepend-left-20
= rm_form.label :enabled, "Remote mirror repository", class: "label-light append-bottom-0"
%p.light.append-bottom-0
......@@ -42,7 +42,7 @@
= render "projects/mirrors/instructions"
.form-group
= rm_form.check_box :only_protected_branches, class: 'pull-left'
= rm_form.check_box :only_protected_branches, class: 'float-left'
.prepend-left-20
= rm_form.label :only_protected_branches, class: 'label-light'
= link_to icon('question-circle'), help_page_path('user/project/protected_branches')
......
......@@ -4,7 +4,7 @@
.card
.card-header
Domains (#{@domains.count})
%ul.well-list.pages-domain-list{ class: ("has-verification-status" if verification_enabled) }
%ul.content-list.pages-domain-list{ class: ("has-verification-status" if verification_enabled) }
- @domains.each do |domain|
%li.pages-domain-list-item.unstyled
- if verification_enabled
......
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.
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.
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