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 ...@@ -6,7 +6,7 @@ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.7-golang-1.9-git
- gitlab-org - gitlab-org
.default-cache: &default-cache .default-cache: &default-cache
key: "ruby-2.3.7-with-yarn" key: "ruby-2.3.7-debian-stretch-with-yarn"
paths: paths:
- vendor/ruby - vendor/ruby
- .yarn-cache/ - .yarn-cache/
...@@ -550,7 +550,7 @@ static-analysis: ...@@ -550,7 +550,7 @@ static-analysis:
script: script:
- scripts/static-analysis - scripts/static-analysis
cache: cache:
key: "ruby-2.3.7-with-yarn-and-rubocop" key: "ruby-2.3.7-debian-stretch-with-yarn-and-rubocop"
paths: paths:
- vendor/ruby - vendor/ruby
- .yarn-cache/ - .yarn-cache/
......
...@@ -2,6 +2,15 @@ ...@@ -2,6 +2,15 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. 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) ## 10.8.1 (2018-05-23)
### Fixed (9 changes) ### Fixed (9 changes)
...@@ -193,6 +202,15 @@ entry. ...@@ -193,6 +202,15 @@ entry.
- Gitaly handles repository forks by default. - 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) ## 10.7.4 (2018-05-21)
### Fixed (1 change) ### Fixed (1 change)
...@@ -457,6 +475,16 @@ entry. ...@@ -457,6 +475,16 @@ entry.
- Upgrade Gitaly to upgrade its charlock_holmes. - 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) ## 10.6.5 (2018-04-24)
### Security (1 change) ### Security (1 change)
......
...@@ -181,7 +181,7 @@ Team labels specify what team is responsible for this issue. ...@@ -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 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, ~Quality, The current team labels are ~Distribution, ~"CI/CD", ~Discussion, ~Documentation, ~Quality,
~Geo, ~Gitaly, ~Monitoring, ~Platform, ~Release, ~"Security Products" 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
......
...@@ -133,7 +133,7 @@ gem 'gitlab-markup', '~> 1.6.2' ...@@ -133,7 +133,7 @@ gem 'gitlab-markup', '~> 1.6.2'
gem 'redcarpet', '~> 3.4' gem 'redcarpet', '~> 3.4'
gem 'commonmarker', '~> 0.17' gem 'commonmarker', '~> 0.17'
gem 'RedCloth', '~> 4.3.2' gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~> 4.2' gem 'rdoc', '~> 6.0'
gem 'org-ruby', '~> 0.9.12' gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0' gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1' gem 'wikicloth', '0.8.1'
...@@ -162,7 +162,7 @@ gem 'acts-as-taggable-on', '~> 5.0' ...@@ -162,7 +162,7 @@ gem 'acts-as-taggable-on', '~> 5.0'
# Background jobs # Background jobs
gem 'sidekiq', '~> 5.1' gem 'sidekiq', '~> 5.1'
gem 'sidekiq-cron', '~> 0.6.0' 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 gem 'sidekiq-limit_fetch', '~> 3.4', require: false
# Cron Parser # Cron Parser
...@@ -412,7 +412,7 @@ group :ed25519 do ...@@ -412,7 +412,7 @@ group :ed25519 do
end end
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly-proto', '~> 0.99.0', require: 'gitaly' gem 'gitaly-proto', '~> 0.100.0', require: 'gitaly'
gem 'grpc', '~> 1.11.0' gem 'grpc', '~> 1.11.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed # Locked until https://github.com/google/protobuf/issues/4210 is closed
......
...@@ -281,7 +281,7 @@ GEM ...@@ -281,7 +281,7 @@ GEM
gettext_i18n_rails (>= 0.7.1) gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0) po_to_json (>= 1.0.0)
rails (>= 3.2.0) rails (>= 3.2.0)
gitaly-proto (0.99.0) gitaly-proto (0.100.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.10) grpc (~> 1.10)
github-linguist (5.3.3) github-linguist (5.3.3)
...@@ -694,8 +694,7 @@ GEM ...@@ -694,8 +694,7 @@ GEM
ffi ffi
rbnacl-libsodium (1.0.11) rbnacl-libsodium (1.0.11)
rbnacl (>= 3.0.1) rbnacl (>= 3.0.1)
rdoc (4.2.2) rdoc (6.0.4)
json (~> 1.4)
re2 (1.1.1) re2 (1.1.1)
recaptcha (3.0.0) recaptcha (3.0.0)
json json
...@@ -709,8 +708,8 @@ GEM ...@@ -709,8 +708,8 @@ GEM
redis-activesupport (5.0.4) redis-activesupport (5.0.4)
activesupport (>= 3, < 6) activesupport (>= 3, < 6)
redis-store (>= 1.3, < 2) redis-store (>= 1.3, < 2)
redis-namespace (1.5.2) redis-namespace (1.6.0)
redis (~> 3.0, >= 3.0.4) redis (>= 3.0.4)
redis-rack (2.0.4) redis-rack (2.0.4)
rack (>= 1.5, < 3) rack (>= 1.5, < 3)
redis-store (>= 1.2, < 2) redis-store (>= 1.2, < 2)
...@@ -801,7 +800,7 @@ GEM ...@@ -801,7 +800,7 @@ GEM
rubyzip (1.2.1) rubyzip (1.2.1)
rufus-scheduler (3.4.0) rufus-scheduler (3.4.0)
et-orbi (~> 1.0) et-orbi (~> 1.0)
rugged (0.27.0) rugged (0.27.1)
safe_yaml (1.0.4) safe_yaml (1.0.4)
sanitize (2.1.0) sanitize (2.1.0)
nokogiri (>= 1.4.4) nokogiri (>= 1.4.4)
...@@ -918,7 +917,7 @@ GEM ...@@ -918,7 +917,7 @@ GEM
unf (0.1.4) unf (0.1.4)
unf_ext unf_ext
unf_ext (0.0.7.5) unf_ext (0.0.7.5)
unicode-display_width (1.3.0) unicode-display_width (1.3.2)
unicorn (5.1.0) unicorn (5.1.0)
kgio (~> 2.6) kgio (~> 2.6)
raindrops (~> 0.7) raindrops (~> 0.7)
...@@ -1036,7 +1035,7 @@ DEPENDENCIES ...@@ -1036,7 +1035,7 @@ DEPENDENCIES
gettext (~> 3.2.2) gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0) gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3) gettext_i18n_rails_js (~> 1.3)
gitaly-proto (~> 0.99.0) gitaly-proto (~> 0.100.0)
github-linguist (~> 5.3.3) github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2) gitlab-gollum-lib (~> 4.2)
...@@ -1124,12 +1123,12 @@ DEPENDENCIES ...@@ -1124,12 +1123,12 @@ DEPENDENCIES
rblineprof (~> 0.3.6) rblineprof (~> 0.3.6)
rbnacl (~> 4.0) rbnacl (~> 4.0)
rbnacl-libsodium rbnacl-libsodium
rdoc (~> 4.2) rdoc (~> 6.0)
re2 (~> 1.1.1) re2 (~> 1.1.1)
recaptcha (~> 3.0) recaptcha (~> 3.0)
redcarpet (~> 3.4) redcarpet (~> 3.4)
redis (~> 3.2) redis (~> 3.2)
redis-namespace (~> 1.5.2) redis-namespace (~> 1.6.0)
redis-rails (~> 5.0.2) redis-rails (~> 5.0.2)
request_store (~> 1.3) request_store (~> 1.3)
responders (~> 2.0) responders (~> 2.0)
......
...@@ -160,7 +160,7 @@ export default { ...@@ -160,7 +160,7 @@ export default {
@input="debouncedPreview" @input="debouncedPreview"
/> />
<span <span
class="help-block" class="form-text text-muted"
v-html="helpText" v-html="helpText"
></span> ></span>
</div> </div>
...@@ -176,7 +176,7 @@ export default { ...@@ -176,7 +176,7 @@ export default {
@input="debouncedPreview" @input="debouncedPreview"
/> />
<span <span
class="help-block" class="form-text text-muted"
v-html="helpText" v-html="helpText"
></span> ></span>
</div> </div>
......
...@@ -43,7 +43,7 @@ export default { ...@@ -43,7 +43,7 @@ export default {
return `${this.changedIcon}-solid`; return `${this.changedIcon}-solid`;
}, },
changedIconClass() { changedIconClass() {
return `multi-${this.changedIcon} pull-left`; return `multi-${this.changedIcon} float-left`;
}, },
tooltipTitle() { tooltipTitle() {
if (!this.showTooltip) return undefined; if (!this.showTooltip) return undefined;
......
...@@ -144,14 +144,14 @@ export default { ...@@ -144,14 +144,14 @@ export default {
<loading-button <loading-button
:loading="submitCommitLoading" :loading="submitCommitLoading"
:disabled="commitButtonDisabled" :disabled="commitButtonDisabled"
container-class="btn btn-success btn-sm pull-left" container-class="btn btn-success btn-sm float-left"
:label="__('Commit')" :label="__('Commit')"
@click="commitChanges" @click="commitChanges"
/> />
<button <button
v-if="!discardDraftButtonDisabled" v-if="!discardDraftButtonDisabled"
type="button" type="button"
class="btn btn-default btn-sm pull-right" class="btn btn-default btn-sm float-right"
@click="discardDraft" @click="discardDraft"
> >
{{ __('Discard draft') }} {{ __('Discard draft') }}
...@@ -159,7 +159,7 @@ export default { ...@@ -159,7 +159,7 @@ export default {
<button <button
v-else v-else
type="button" type="button"
class="btn btn-default btn-sm pull-right" class="btn btn-default btn-sm float-right"
@click="toggleIsSmall" @click="toggleIsSmall"
> >
{{ __('Collapse') }} {{ __('Collapse') }}
......
...@@ -120,7 +120,7 @@ export default { ...@@ -120,7 +120,7 @@ export default {
</ul> </ul>
<p <p
v-else v-else
class="multi-file-commit-list help-block" class="multi-file-commit-list form-text text-muted"
> >
{{ __('No changes') }} {{ __('No changes') }}
</p> </p>
......
...@@ -80,7 +80,7 @@ export default { ...@@ -80,7 +80,7 @@ export default {
{{ __('Commit Message') }} {{ __('Commit Message') }}
<span <span
v-popover="$options.popoverOptions" v-popover="$options.popoverOptions"
class="help-block prepend-left-10" class="form-text text-muted prepend-left-10"
> >
<icon <icon
name="question" name="question"
......
...@@ -72,10 +72,9 @@ export default { ...@@ -72,10 +72,9 @@ export default {
<form <form
slot="body" slot="body"
@submit.prevent="createEntryInStore" @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">
<label class="label-light col-form-label col-sm-3 ide-new-modal-label">
{{ __('Name') }} {{ __('Name') }}
</label> </label>
<div class="col-sm-9"> <div class="col-sm-9">
...@@ -86,7 +85,6 @@ export default { ...@@ -86,7 +85,6 @@ export default {
ref="fieldName" ref="fieldName"
/> />
</div> </div>
</fieldset>
</form> </form>
</deprecated-modal> </deprecated-modal>
</template> </template>
...@@ -169,7 +169,7 @@ export default { ...@@ -169,7 +169,7 @@ export default {
:show-tooltip="true" :show-tooltip="true"
:show-staged-icon="true" :show-staged-icon="true"
:force-modified-icon="true" :force-modified-icon="true"
class="pull-right" class="float-right"
/> />
</span> </span>
<new-dropdown <new-dropdown
......
...@@ -48,11 +48,10 @@ export default { ...@@ -48,11 +48,10 @@ export default {
return `${this.job.runner.description} (#${this.job.runner.id})`; return `${this.job.runner.description} (#${this.job.runner.id})`;
}, },
retryButtonClass() { 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 += className +=
this.job.status && this.job.recoverable this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary';
? ' btn-primary'
: ' btn-inverted-secondary';
return className; return className;
}, },
hasTimeout() { hasTimeout() {
...@@ -104,8 +103,7 @@ export default { ...@@ -104,8 +103,7 @@ export default {
<button <button
type="button" type="button"
:aria-label="__('Toggle Sidebar')" :aria-label="__('Toggle Sidebar')"
class="btn btn-blank gutter-toggle pull-right class="btn btn-blank gutter-toggle float-right d-block d-md-none js-sidebar-build-toggle"
d-block d-sm-block d-md-none js-sidebar-build-toggle"
> >
<i <i
aria-hidden="true" aria-hidden="true"
......
...@@ -362,7 +362,7 @@ export default class MergeRequestTabs { ...@@ -362,7 +362,7 @@ export default class MergeRequestTabs {
// //
// status - Boolean, true to show, false to hide // status - Boolean, true to show, false to hide
toggleLoading(status) { toggleLoading(status) {
$('.mr-loading-status .loading').toggleClass('hidden', status); $('.mr-loading-status .loading').toggleClass('hidden', !status);
} }
diffViewType() { diffViewType() {
......
import groupAvatar from '~/group_avatar'; import groupAvatar from '~/group_avatar';
import TransferDropdown from '~/groups/transfer_dropdown'; import TransferDropdown from '~/groups/transfer_dropdown';
import initConfirmDangerModal from '~/confirm_danger_modal'; import initConfirmDangerModal from '~/confirm_danger_modal';
import initSettingsPanels from '~/settings_panels';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
groupAvatar(); groupAvatar();
new TransferDropdown(); // eslint-disable-line no-new new TransferDropdown(); // eslint-disable-line no-new
initConfirmDangerModal(); initConfirmDangerModal();
}); });
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
initSettingsPanels();
});
import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
document.addEventListener('DOMContentLoaded', () => {
initGkeDropdowns();
});
...@@ -213,7 +213,7 @@ ...@@ -213,7 +213,7 @@
</i> </i>
</div> </div>
</div> </div>
<span class="help-block">{{ visibilityLevelDescription }}</span> <span class="form-text text-muted">{{ visibilityLevelDescription }}</span>
<label <label
v-if="visibilityLevel !== visibilityOptions.PRIVATE" v-if="visibilityLevel !== visibilityOptions.PRIVATE"
class="request-access" class="request-access"
......
import bp from '../../../breakpoints'; import bp from '../../../breakpoints';
import { slugify } from '../../../lib/utils/text_utility'; 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 { export default class Wikis {
constructor() { constructor() {
...@@ -28,7 +30,12 @@ export default class Wikis { ...@@ -28,7 +30,12 @@ export default class Wikis {
if (slug.length > 0) { if (slug.length > 0) {
const wikisPath = slugInput.getAttribute('data-wikis-path'); 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(); e.preventDefault();
} }
} }
......
...@@ -77,8 +77,7 @@ export default class UserTabs { ...@@ -77,8 +77,7 @@ export default class UserTabs {
this.action = action || this.defaultAction; this.action = action || this.defaultAction;
this.$parentEl = $(parentEl) || $(document); this.$parentEl = $(parentEl) || $(document);
this.windowLocation = window.location; this.windowLocation = window.location;
this.$parentEl.find('.nav-links a') this.$parentEl.find('.nav-links a').each((i, navLink) => {
.each((i, navLink) => {
this.loaded[$(navLink).attr('data-action')] = false; this.loaded[$(navLink).attr('data-action')] = false;
}); });
this.actions = Object.keys(this.loaded); this.actions = Object.keys(this.loaded);
...@@ -116,8 +115,7 @@ export default class UserTabs { ...@@ -116,8 +115,7 @@ export default class UserTabs {
} }
activateTab(action) { activateTab(action) {
return this.$parentEl.find(`.nav-links .js-${action}-tab a`) return this.$parentEl.find(`.nav-links .js-${action}-tab a`).tab('show');
.tab('show');
} }
setTab(action, endpoint) { setTab(action, endpoint) {
...@@ -137,7 +135,8 @@ export default class UserTabs { ...@@ -137,7 +135,8 @@ export default class UserTabs {
loadTab(action, endpoint) { loadTab(action, endpoint) {
this.toggleLoading(true); this.toggleLoading(true);
return axios.get(endpoint) return axios
.get(endpoint)
.then(({ data }) => { .then(({ data }) => {
const tabSelector = `div#${action}`; const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html); this.$parentEl.find(tabSelector).html(data.html);
...@@ -161,10 +160,11 @@ export default class UserTabs { ...@@ -161,10 +160,11 @@ export default class UserTabs {
const utcOffset = $calendarWrap.data('utcOffset'); const utcOffset = $calendarWrap.data('utcOffset');
let utcFormatted = 'UTC'; let utcFormatted = 'UTC';
if (utcOffset !== 0) { if (utcOffset !== 0) {
utcFormatted = `UTC${utcOffset > 0 ? '+' : ''}${(utcOffset / 3600)}`; utcFormatted = `UTC${utcOffset > 0 ? '+' : ''}${utcOffset / 3600}`;
} }
axios.get(calendarPath) axios
.get(calendarPath)
.then(({ data }) => { .then(({ data }) => {
$calendarWrap.html(CALENDAR_TEMPLATE); $calendarWrap.html(CALENDAR_TEMPLATE);
$calendarWrap.find('.calendar-hint').append(`(Timezone: ${utcFormatted})`); $calendarWrap.find('.calendar-hint').append(`(Timezone: ${utcFormatted})`);
...@@ -180,17 +180,20 @@ export default class UserTabs { ...@@ -180,17 +180,20 @@ export default class UserTabs {
} }
toggleLoading(status) { toggleLoading(status) {
return this.$parentEl.find('.loading-status .loading') return this.$parentEl.find('.loading-status .loading').toggleClass('hidden', !status);
.toggleClass('hidden', status);
} }
setCurrentAction(source) { setCurrentAction(source) {
let newState = source; let newState = source;
newState = newState.replace(/\/+$/, ''); newState = newState.replace(/\/+$/, '');
newState += this.windowLocation.search + this.windowLocation.hash; newState += this.windowLocation.search + this.windowLocation.hash;
history.replaceState({ history.replaceState(
{
url: newState, url: newState,
}, document.title, newState); },
document.title,
newState,
);
return newState; return newState;
} }
......
...@@ -99,7 +99,7 @@ Please update your Git repository remotes as soon as possible.`), ...@@ -99,7 +99,7 @@ Please update your Git repository remotes as soon as possible.`),
:disabled="isRequestPending" :disabled="isRequestPending"
/> />
</div> </div>
<p class="help-block"> <p class="form-text text-muted">
{{ path }} {{ path }}
</p> </p>
</div> </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 { ...@@ -5,8 +5,8 @@ export default {
type: String, type: String,
required: true, required: true,
}, },
label: { value: {
type: Object, type: [Number, String],
required: true, required: true,
}, },
}, },
...@@ -17,6 +17,6 @@ export default { ...@@ -17,6 +17,6 @@ export default {
<input <input
type="hidden" type="hidden"
:name="name" :name="name"
:value="label.id" :value="value"
/> />
</template> </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 @@ ...@@ -2,13 +2,13 @@
import $ from 'jquery'; import $ from 'jquery';
import { __ } from '~/locale'; import { __ } from '~/locale';
import LabelsSelect from '~/labels_select'; import LabelsSelect from '~/labels_select';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import LoadingIcon from '../../loading_icon.vue'; import LoadingIcon from '../../loading_icon.vue';
import DropdownTitle from './dropdown_title.vue'; import DropdownTitle from './dropdown_title.vue';
import DropdownValue from './dropdown_value.vue'; import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue'; import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
import DropdownButton from './dropdown_button.vue'; import DropdownButton from './dropdown_button.vue';
import DropdownHiddenInput from './dropdown_hidden_input.vue';
import DropdownHeader from './dropdown_header.vue'; import DropdownHeader from './dropdown_header.vue';
import DropdownSearchInput from './dropdown_search_input.vue'; import DropdownSearchInput from './dropdown_search_input.vue';
import DropdownFooter from './dropdown_footer.vue'; import DropdownFooter from './dropdown_footer.vue';
...@@ -140,7 +140,7 @@ export default { ...@@ -140,7 +140,7 @@ export default {
v-for="label in context.labels" v-for="label in context.labels"
:key="label.id" :key="label.id"
:name="hiddenInputName" :name="hiddenInputName"
:label="label" :value="label.id"
/> />
<div <div
class="dropdown" class="dropdown"
......
...@@ -131,6 +131,10 @@ table { ...@@ -131,6 +131,10 @@ table {
} }
.card { .card {
.card-title {
margin-bottom: 0;
}
&.card-without-border { &.card-without-border {
@extend .border-0; @extend .border-0;
} }
...@@ -147,3 +151,7 @@ table { ...@@ -147,3 +151,7 @@ table {
.nav-tabs .nav-link { .nav-tabs .nav-link {
border: 0; border: 0;
} }
pre code {
white-space: pre-wrap;
}
...@@ -63,6 +63,10 @@ ...@@ -63,6 +63,10 @@
border-radius: $border-radius-base; border-radius: $border-radius-base;
white-space: nowrap; white-space: nowrap;
&:disabled.read-only {
color: $gl-text-color !important;
}
&.no-outline { &.no-outline {
outline: 0; outline: 0;
} }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
* Well styled list * Well styled list
* *
*/ */
.card-body-list { .hover-list {
position: relative; position: relative;
margin: 0; margin: 0;
padding: 0; padding: 0;
......
...@@ -405,7 +405,7 @@ table.u2f-registrations { ...@@ -405,7 +405,7 @@ table.u2f-registrations {
margin-right: $gl-padding / 4; margin-right: $gl-padding / 4;
} }
.label-verification-status { .badge-verification-status {
border-width: 1px; border-width: 1px;
border-style: solid; border-style: solid;
......
...@@ -546,7 +546,7 @@ ...@@ -546,7 +546,7 @@
margin-right: 0; margin-right: 0;
} }
&.help-block { &.form-text.text-muted {
margin-left: 0; margin-left: 0;
right: 0; right: 0;
} }
...@@ -952,7 +952,7 @@ ...@@ -952,7 +952,7 @@
height: 30px; height: 30px;
} }
.help-block { .form-text.text-muted {
margin-top: 2px; margin-top: 2px;
color: $blue-500; color: $blue-500;
cursor: pointer; cursor: pointer;
...@@ -1088,10 +1088,6 @@ ...@@ -1088,10 +1088,6 @@
font-size: 12px; font-size: 12px;
} }
.ide-new-modal-label {
line-height: 34px;
}
.multi-file-commit-panel-success-message { .multi-file-commit-panel-success-message {
position: absolute; position: absolute;
top: 61px; top: 61px;
......
class Admin::DashboardController < Admin::ApplicationController class Admin::DashboardController < Admin::ApplicationController
include CountHelper include CountHelper
COUNTED_ITEMS = [Project, User, Group, ForkedProjectLink, Issue, MergeRequest,
Note, Snippet, Key, Milestone].freeze
def index def index
@counts = Gitlab::Database::Count.approximate_counts(COUNTED_ITEMS)
@projects = Project.order_id_desc.without_deleted.with_route.limit(10) @projects = Project.order_id_desc.without_deleted.with_route.limit(10)
@users = User.order_id_desc.limit(10) @users = User.order_id_desc.limit(10)
@groups = Group.order_id_desc.with_route.limit(10) @groups = Group.order_id_desc.with_route.limit(10)
......
...@@ -93,8 +93,6 @@ class ProfilesController < Profiles::ApplicationController ...@@ -93,8 +93,6 @@ class ProfilesController < Profiles::ApplicationController
:linkedin, :linkedin,
:location, :location,
:name, :name,
:password,
:password_confirmation,
:public_email, :public_email,
:skype, :skype,
:twitter, :twitter,
......
class Projects::Clusters::GcpController < Projects::ApplicationController class Projects::Clusters::GcpController < Projects::ApplicationController
before_action :authorize_read_cluster! 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 :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 def login
begin begin
...@@ -37,21 +36,6 @@ class Projects::Clusters::GcpController < Projects::ApplicationController ...@@ -37,21 +36,6 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
private 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 def create_params
params.require(:cluster).permit( params.require(:cluster).permit(
:enabled, :enabled,
...@@ -75,17 +59,7 @@ class Projects::Clusters::GcpController < Projects::ApplicationController ...@@ -75,17 +59,7 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
end end
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 def token_in_session
@token_in_session ||=
session[GoogleApi::CloudPlatform::Client.session_key_for_token] session[GoogleApi::CloudPlatform::Client.session_key_for_token]
end end
......
...@@ -71,19 +71,6 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -71,19 +71,6 @@ class Projects::ClustersController < Projects::ApplicationController
.present(current_user: current_user) .present(current_user: current_user)
end 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 def update_params
if cluster.managed? if cluster.managed?
params.require(:cluster).permit( params.require(:cluster).permit(
......
...@@ -7,6 +7,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -7,6 +7,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize] 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 :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics]
before_action :verify_api_request!, only: :terminal_websocket_authorize before_action :verify_api_request!, only: :terminal_websocket_authorize
before_action :expire_etag_cache, only: [:index]
def index def index
@environments = project.environments @environments = project.environments
...@@ -148,6 +149,15 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -148,6 +149,15 @@ class Projects::EnvironmentsController < Projects::ApplicationController
Gitlab::Workhorse.verify_api_request!(request.headers) Gitlab::Workhorse.verify_api_request!(request.headers)
end 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 def environment_params
params.require(:environment).permit(:name, :external_url) params.require(:environment).permit(:name, :external_url)
end end
......
...@@ -14,6 +14,8 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -14,6 +14,8 @@ class Projects::WikisController < Projects::ApplicationController
def show def show
@page = @project_wiki.find_page(params[:id], params[:version_id]) @page = @project_wiki.find_page(params[:id], params[:version_id])
view_param = @project_wiki.empty? ? params[:view] : 'create'
if @page if @page
render 'show' render 'show'
elsif file = @project_wiki.find_file(params[:id], params[:version_id]) elsif file = @project_wiki.find_file(params[:id], params[:version_id])
...@@ -26,12 +28,12 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -26,12 +28,12 @@ class Projects::WikisController < Projects::ApplicationController
disposition: 'inline', disposition: 'inline',
filename: file.name filename: file.name
) )
else elsif can?(current_user, :create_wiki, @project) && view_param == 'create'
return render('empty') unless can?(current_user, :create_wiki, @project)
@page = build_page(title: params[:id]) @page = build_page(title: params[:id])
render 'edit' render 'edit'
else
render 'empty'
end end
end end
......
...@@ -39,15 +39,6 @@ class GroupProjectsFinder < ProjectsFinder ...@@ -39,15 +39,6 @@ class GroupProjectsFinder < ProjectsFinder
end end
def collection_with_user 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
else
if only_shared? if only_shared?
[shared_projects.public_or_visible_to_user(current_user)] [shared_projects.public_or_visible_to_user(current_user)]
elsif only_owned? elsif only_owned?
...@@ -59,7 +50,6 @@ class GroupProjectsFinder < ProjectsFinder ...@@ -59,7 +50,6 @@ class GroupProjectsFinder < ProjectsFinder
] ]
end end
end end
end
def collection_without_user def collection_without_user
if only_shared? if only_shared?
......
...@@ -204,7 +204,7 @@ module ApplicationSettingsHelper ...@@ -204,7 +204,7 @@ module ApplicationSettingsHelper
:pages_domain_verification_enabled, :pages_domain_verification_enabled,
:password_authentication_enabled_for_web, :password_authentication_enabled_for_web,
:password_authentication_enabled_for_git, :password_authentication_enabled_for_git,
:performance_bar_allowed_group_id, :performance_bar_allowed_group_path,
:performance_bar_enabled, :performance_bar_enabled,
:plantuml_enabled, :plantuml_enabled,
:plantuml_url, :plantuml_url,
......
module CountHelper module CountHelper
def approximate_count_with_delimiters(model) def approximate_count_with_delimiters(count_data, model)
number_with_delimiter(Gitlab::Database::Count.approximate_count(model)) count = count_data[model]
raise "Missing model #{model} from count data" unless count
number_with_delimiter(count)
end end
end end
...@@ -11,6 +11,7 @@ module NavHelper ...@@ -11,6 +11,7 @@ module NavHelper
class_name = page_gutter_class class_name = page_gutter_class
class_name << 'page-with-contextual-sidebar' if defined?(@left_sidebar) && @left_sidebar 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 << 'page-with-icon-sidebar' if collapsed_sidebar? && @left_sidebar
class_name -= ['right-sidebar-expanded'] if defined?(@right_sidebar) && !@right_sidebar
class_name class_name
end end
......
...@@ -257,6 +257,9 @@ module ProjectsHelper ...@@ -257,6 +257,9 @@ module ProjectsHelper
if project.builds_enabled? && can?(current_user, :read_pipeline, project) if project.builds_enabled? && can?(current_user, :read_pipeline, project)
nav_tabs << :pipelines nav_tabs << :pipelines
end
if can?(current_user, :read_environment, project) || can?(current_user, :read_cluster, project)
nav_tabs << :operations nav_tabs << :operations
end end
......
...@@ -230,6 +230,7 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -230,6 +230,7 @@ class ApplicationSetting < ActiveRecord::Base
after_commit do after_commit do
reset_memoized_terms reset_memoized_terms
end end
after_commit :expire_performance_bar_allowed_user_ids_cache, if: -> { previous_changes.key?('performance_bar_allowed_group_id') }
def self.defaults def self.defaults
{ {
...@@ -386,31 +387,6 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -386,31 +387,6 @@ class ApplicationSetting < ActiveRecord::Base
super(levels.map { |level| Gitlab::VisibilityLevel.level_value(level) }) super(levels.map { |level| Gitlab::VisibilityLevel.level_value(level) })
end 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 def performance_bar_allowed_group
Group.find_by_id(performance_bar_allowed_group_id) Group.find_by_id(performance_bar_allowed_group_id)
end end
...@@ -420,15 +396,6 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -420,15 +396,6 @@ class ApplicationSetting < ActiveRecord::Base
performance_bar_allowed_group_id.present? performance_bar_allowed_group_id.present?
end 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 # Choose one of the available repository storage options. Currently all have
# equal weighting. # equal weighting.
def pick_repository_storage def pick_repository_storage
...@@ -506,4 +473,8 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -506,4 +473,8 @@ class ApplicationSetting < ActiveRecord::Base
errors.add(:terms, "You need to set terms to be enforced") unless terms.present? errors.add(:terms, "You need to set terms to be enforced") unless terms.present?
end end
def expire_performance_bar_allowed_user_ids_cache
Gitlab::PerformanceBar.expire_allowed_user_ids_cache
end
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 @@ ...@@ -3,6 +3,7 @@
# A note of this type can be resolvable. # A note of this type can be resolvable.
class DiffNote < Note class DiffNote < Note
include NoteOnDiff include NoteOnDiff
include Gitlab::Utils::StrongMemoize
NOTEABLE_TYPES = %w(MergeRequest Commit).freeze NOTEABLE_TYPES = %w(MergeRequest Commit).freeze
...@@ -12,7 +13,6 @@ class DiffNote < Note ...@@ -12,7 +13,6 @@ class DiffNote < Note
validates :original_position, presence: true validates :original_position, presence: true
validates :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 :line_code, presence: true, line_code: true, if: :on_text?
validates :noteable_type, inclusion: { in: NOTEABLE_TYPES } validates :noteable_type, inclusion: { in: NOTEABLE_TYPES }
validate :positions_complete validate :positions_complete
...@@ -23,6 +23,7 @@ class DiffNote < Note ...@@ -23,6 +23,7 @@ class DiffNote < Note
before_validation :update_position, on: :create, if: :on_text? before_validation :update_position, on: :create, if: :on_text?
before_validation :set_line_code, if: :on_text? before_validation :set_line_code, if: :on_text?
after_save :keep_around_commits after_save :keep_around_commits
after_commit :create_diff_file, on: :create
def discussion_class(*) def discussion_class(*)
DiffDiscussion DiffDiscussion
...@@ -53,20 +54,24 @@ class DiffNote < Note ...@@ -53,20 +54,24 @@ class DiffNote < Note
position.position_type == "image" position.position_type == "image"
end end
def diff_file def create_diff_file
@diff_file ||= return unless should_create_diff_file?
begin
if created_at_diff?(noteable.diff_refs) diff_file = fetch_diff_file
# We're able to use the already persisted diffs (Postgres) if we're diff_line = diff_file.line_for_position(self.original_position)
# presenting a "current version" of the MR discussion diff.
# So no need to make an extra Gitaly diff request for it. creation_params = diff_file.diff.to_hash
# As an extra benefit, the returned `diff_file` already .except(:too_large)
# has `highlighted_diff_lines` data set from Redis on .merge(diff: diff_file.diff_hunk(diff_line))
# `Diff::FileCollection::MergeRequestDiff`.
noteable.diffs(paths: original_position.paths, expanded: true).diff_files.first create_note_diff_file(creation_params)
else
original_position.diff_file(self.project.repository)
end end
def diff_file
strong_memoize(:diff_file) do
enqueue_diff_file_creation_job if should_create_diff_file?
fetch_diff_file
end end
end end
...@@ -98,6 +103,38 @@ class DiffNote < Note ...@@ -98,6 +103,38 @@ class DiffNote < Note
private 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? def supported?
for_commit? || self.noteable.has_complete_diff_refs? for_commit? || self.noteable.has_complete_diff_refs?
end end
......
...@@ -40,6 +40,7 @@ class Event < ActiveRecord::Base ...@@ -40,6 +40,7 @@ class Event < ActiveRecord::Base
).freeze ).freeze
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour 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 :name, :email, :public_email, :username, to: :author, prefix: true, allow_nil: true
delegate :title, to: :issue, prefix: true, allow_nil: true delegate :title, to: :issue, prefix: true, allow_nil: true
...@@ -391,6 +392,7 @@ class Event < ActiveRecord::Base ...@@ -391,6 +392,7 @@ class Event < ActiveRecord::Base
def set_last_repository_updated_at def set_last_repository_updated_at
Project.unscoped.where(id: project_id) 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) .update_all(last_repository_updated_at: created_at)
end end
......
...@@ -24,12 +24,9 @@ class InternalId < ActiveRecord::Base ...@@ -24,12 +24,9 @@ class InternalId < ActiveRecord::Base
# #
# The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL). # 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. # As such, the increment is atomic and safe to be called concurrently.
# def increment_and_save!
# 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)
lock! lock!
self.last_value = [(last_value || 0) + 1, (maximum_iid || 0) + 1].max self.last_value = (last_value || 0) + 1
save! save!
last_value last_value
end end
...@@ -93,16 +90,7 @@ class InternalId < ActiveRecord::Base ...@@ -93,16 +90,7 @@ class InternalId < ActiveRecord::Base
# and increment its last value # and increment its last value
# #
# Note this will acquire a ROW SHARE lock on the InternalId record # Note this will acquire a ROW SHARE lock on the InternalId record
(lookup || create_record).increment_and_save!
# 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)
end end
end end
...@@ -128,15 +116,11 @@ class InternalId < ActiveRecord::Base ...@@ -128,15 +116,11 @@ class InternalId < ActiveRecord::Base
InternalId.create!( InternalId.create!(
**scope, **scope,
usage: usage_value, usage: usage_value,
last_value: maximum_iid last_value: init.call(subject) || 0
) )
end end
rescue ActiveRecord::RecordNotUnique rescue ActiveRecord::RecordNotUnique
lookup lookup
end end
def maximum_iid
@maximum_iid ||= init.call(subject) || 0
end
end end
end end
class MergeRequestDiffFile < ActiveRecord::Base class MergeRequestDiffFile < ActiveRecord::Base
include Gitlab::EncodingHelper include Gitlab::EncodingHelper
include DiffFile
belongs_to :merge_request_diff belongs_to :merge_request_diff
...@@ -12,10 +13,4 @@ class MergeRequestDiffFile < ActiveRecord::Base ...@@ -12,10 +13,4 @@ class MergeRequestDiffFile < ActiveRecord::Base
def diff def diff
binary? ? super.unpack('m0').first : super binary? ? super.unpack('m0').first : super
end end
def to_hash
keys = Gitlab::Git::Diff::SERIALIZE_KEYS - [:diff]
as_json(only: keys).merge(diff: diff).with_indifferent_access
end
end end
...@@ -63,6 +63,7 @@ class Note < ActiveRecord::Base ...@@ -63,6 +63,7 @@ class Note < ActiveRecord::Base
has_many :todos has_many :todos
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :system_note_metadata 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 :gfm_reference, :local_reference, to: :noteable
delegate :name, to: :project, prefix: true delegate :name, to: :project, prefix: true
...@@ -100,7 +101,8 @@ class Note < ActiveRecord::Base ...@@ -100,7 +101,8 @@ class Note < ActiveRecord::Base
scope :inc_author_project, -> { includes(:project, :author) } scope :inc_author_project, -> { includes(:project, :author) }
scope :inc_author, -> { includes(:author) } scope :inc_author, -> { includes(:author) }
scope :inc_relations_for_view, -> do 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 end
scope :diff_notes, -> { where(type: %w(LegacyDiffNote DiffNote)) } 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 ...@@ -24,6 +24,7 @@ class Project < ActiveRecord::Base
include ChronicDurationAttribute include ChronicDurationAttribute
include FastDestroyAll::Helpers include FastDestroyAll::Helpers
include WithUploads include WithUploads
include BatchDestroyDependentAssociations
extend Gitlab::ConfigHelper extend Gitlab::ConfigHelper
......
...@@ -3,6 +3,10 @@ module ApplicationSettings ...@@ -3,6 +3,10 @@ module ApplicationSettings
def execute def execute
update_terms(@params.delete(:terms)) 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) @application_setting.update(@params)
end end
...@@ -18,5 +22,13 @@ module ApplicationSettings ...@@ -18,5 +22,13 @@ module ApplicationSettings
ApplicationSetting::Term.create(terms: terms) ApplicationSetting::Term.create(terms: terms)
@application_setting.reset_memoized_terms @application_setting.reset_memoized_terms
end 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
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 ...@@ -137,7 +137,13 @@ module Projects
trash_repositories! 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! project.destroy!
end end
end end
......
...@@ -11,7 +11,7 @@ module ObjectStorage ...@@ -11,7 +11,7 @@ module ObjectStorage
ObjectStorageUnavailable = Class.new(StandardError) ObjectStorageUnavailable = Class.new(StandardError)
DIRECT_UPLOAD_TIMEOUT = 4.hours DIRECT_UPLOAD_TIMEOUT = 4.hours
TMP_UPLOAD_PATH = 'tmp/upload'.freeze TMP_UPLOAD_PATH = 'tmp/uploads'.freeze
module Store module Store
LOCAL = 1 LOCAL = 1
......
...@@ -9,8 +9,8 @@ ...@@ -9,8 +9,8 @@
= f.check_box :performance_bar_enabled = f.check_box :performance_bar_enabled
Enable the Performance Bar Enable the Performance Bar
.form-group.row .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 .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" = f.submit 'Save changes', class: "btn btn-success"
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
= f.label :mirror_available do = f.label :mirror_available do
= f.check_box :mirror_available = f.check_box :mirror_available
Allow mirrors to be setup for projects 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. If disabled, only admins will be able to setup mirrors in projects.
= link_to icon('question-circle'), help_page_path('workflow/repository_mirroring') = link_to icon('question-circle'), help_page_path('workflow/repository_mirroring')
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
must be used to authenticate. must be used to authenticate.
- if omniauth_enabled? && button_based_providers.any? - if omniauth_enabled? && button_based_providers.any?
.form-group.row .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][]' = hidden_field_tag 'application_setting[enabled_oauth_sign_in_sources][]'
.col-sm-10 .col-sm-10
.btn-group{ data: { toggle: 'buttons' } } .btn-group{ data: { toggle: 'buttons' } }
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
= f.label :enforce_terms do = f.label :enforce_terms do
= f.check_box :enforce_terms = f.check_box :enforce_terms
= _("Require all users to accept Terms of Service when they access GitLab.") = _("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.") = _("When enabled, users cannot use GitLab until the terms have been accepted.")
.form-group .form-group
.col-sm-12 .col-sm-12
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
= _("Terms of Service Agreement") = _("Terms of Service Agreement")
.col-sm-12 .col-sm-12
= f.text_area :terms, class: 'form-control', rows: 8 = f.text_area :terms, class: 'form-control', rows: 8
.help-block .form-text.text-muted
= _("Markdown enabled") = _("Markdown enabled")
= f.submit _("Save changes"), class: "btn btn-success" = f.submit _("Save changes"), class: "btn btn-success"
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
.form-check .form-check
= level = level
%span.form-text.text-muted#restricted-visibility-help %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. If the public level is restricted, user profiles are only visible to logged in users.
.form-group.row .form-group.row
= f.label :import_sources, class: 'col-form-label col-sm-2' = f.label :import_sources, class: 'col-form-label col-sm-2'
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
= link_to admin_projects_path do = link_to admin_projects_path do
%h3.text-center %h3.text-center
Projects: Projects:
= approximate_count_with_delimiters(Project) = approximate_count_with_delimiters(@counts, Project)
%hr %hr
= link_to('New project', new_project_path, class: "btn btn-new") = link_to('New project', new_project_path, class: "btn btn-new")
.col-sm-4 .col-sm-4
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
= link_to admin_users_path do = link_to admin_users_path do
%h3.text-center %h3.text-center
Users: Users:
= approximate_count_with_delimiters(User) = approximate_count_with_delimiters(@counts, User)
= render_if_exists 'users_statistics' = render_if_exists 'users_statistics'
%hr %hr
= link_to 'New user', new_admin_user_path, class: "btn btn-new" = link_to 'New user', new_admin_user_path, class: "btn btn-new"
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
= link_to admin_groups_path do = link_to admin_groups_path do
%h3.text-center %h3.text-center
Groups: Groups:
= approximate_count_with_delimiters(Group) = approximate_count_with_delimiters(@counts, Group)
%hr %hr
= link_to 'New group', new_admin_group_path, class: "btn btn-new" = link_to 'New group', new_admin_group_path, class: "btn btn-new"
.row .row
...@@ -42,31 +42,31 @@ ...@@ -42,31 +42,31 @@
%p %p
Forks Forks
%span.light.float-right %span.light.float-right
= approximate_count_with_delimiters(ForkedProjectLink) = approximate_count_with_delimiters(@counts, ForkedProjectLink)
%p %p
Issues Issues
%span.light.float-right %span.light.float-right
= approximate_count_with_delimiters(Issue) = approximate_count_with_delimiters(@counts, Issue)
%p %p
Merge Requests Merge Requests
%span.light.float-right %span.light.float-right
= approximate_count_with_delimiters(MergeRequest) = approximate_count_with_delimiters(@counts, MergeRequest)
%p %p
Notes Notes
%span.light.float-right %span.light.float-right
= approximate_count_with_delimiters(Note) = approximate_count_with_delimiters(@counts, Note)
%p %p
Snippets Snippets
%span.light.float-right %span.light.float-right
= approximate_count_with_delimiters(Snippet) = approximate_count_with_delimiters(@counts, Snippet)
%p %p
SSH Keys SSH Keys
%span.light.float-right %span.light.float-right
= approximate_count_with_delimiters(Key) = approximate_count_with_delimiters(@counts, Key)
%p %p
Milestones Milestones
%span.light.float-right %span.light.float-right
= approximate_count_with_delimiters(Milestone) = approximate_count_with_delimiters(@counts, Milestone)
%p %p
Active Users Active Users
%span.light.float-right %span.light.float-right
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
.card .card
.card-header .card-header
Group info: Group info:
%ul.well-list %ul.content-list
%li %li
.avatar-container.s60 .avatar-container.s60
= group_icon(@group, class: "avatar s60") = group_icon(@group, class: "avatar s60")
...@@ -64,7 +64,7 @@ ...@@ -64,7 +64,7 @@
Projects Projects
%span.badge.badge-pill %span.badge.badge-pill
#{@group.projects.count} #{@group.projects.count}
%ul.well-list %ul.content-list
- @projects.each do |project| - @projects.each do |project|
%li %li
%strong %strong
...@@ -82,7 +82,7 @@ ...@@ -82,7 +82,7 @@
Projects shared with #{@group.name} Projects shared with #{@group.name}
%span.badge.badge-pill %span.badge.badge-pill
#{@group.shared_projects.count} #{@group.shared_projects.count}
%ul.well-list %ul.content-list
- @group.shared_projects.sort_by(&:name).each do |project| - @group.shared_projects.sort_by(&:name).each do |project|
%li %li
%strong %strong
...@@ -118,7 +118,7 @@ ...@@ -118,7 +118,7 @@
%span.badge.badge-pill= @group.members.size %span.badge.badge-pill= @group.members.size
.float-right .float-right
= link_to icon('pencil-square-o', text: 'Manage access'), polymorphic_url([@group, :members]), class: "btn btn-sm" = 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 } = render partial: 'shared/members/member', collection: @members, as: :member, locals: { show_controls: false }
.card-footer .card-footer
= paginate @members, param_name: 'members_page', theme: 'gitlab' = paginate @members, param_name: 'members_page', theme: 'gitlab'
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
.card .card
.card-header .card-header
Project info: Project info:
%ul.well-list %ul.content-list
%li %li
%span.light Name: %span.light Name:
%strong %strong
...@@ -166,7 +166,7 @@ ...@@ -166,7 +166,7 @@
.float-right .float-right
= link_to admin_group_path(@group), class: 'btn btn-sm' do = link_to admin_group_path(@group), class: 'btn btn-sm' do
= icon('pencil-square-o', text: 'Manage access') = 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 } = render partial: 'shared/members/member', collection: @group_members, as: :member, locals: { show_controls: false }
.card-footer .card-footer
= paginate @group_members, param_name: 'group_members_page', theme: 'gitlab' = paginate @group_members, param_name: 'group_members_page', theme: 'gitlab'
...@@ -180,7 +180,7 @@ ...@@ -180,7 +180,7 @@
%span.badge.badge-pill= @project.users.size %span.badge.badge-pill= @project.users.size
.float-right .float-right
= link_to icon('pencil-square-o', text: 'Manage access'), polymorphic_url([@project, :members]), class: "btn btn-sm" = 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 } = render partial: 'shared/members/member', collection: @project_members, as: :member, locals: { show_controls: false }
.card-footer .card-footer
= paginate @project_members, param_name: 'project_members_page', theme: 'gitlab' = paginate @project_members, param_name: 'project_members_page', theme: 'gitlab'
.card .card
.card-header .card-header
Profile Profile
%ul.well-list %ul.content-list
%li %li
%span.light Member since %span.light Member since
%strong= user.created_at.to_s(:medium) %strong= user.created_at.to_s(:medium)
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
- if @user.groups.any? - if @user.groups.any?
.card .card
.card-header Group projects .card-header Group projects
%ul.card-body-list %ul.hover-list
- @user.group_members.includes(:source).each do |group_member| - @user.group_members.includes(:source).each do |group_member|
- group = group_member.group - group = group_member.group
%li.group_member %li.group_member
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
.col-md-6 .col-md-6
.card .card
.card-header Joined projects (#{@joined_projects.count}) .card-header Joined projects (#{@joined_projects.count})
%ul.card-body-list %ul.hover-list
- @joined_projects.sort_by(&:full_name).each do |project| - @joined_projects.sort_by(&:full_name).each do |project|
- member = project.team.find_member(@user.id) - member = project.team.find_member(@user.id)
%li.project_member %li.project_member
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
.card .card
.card-header .card-header
= @user.name = @user.name
%ul.well-list %ul.content-list
%li %li
= image_tag avatar_icon_for_user(@user, 60), class: "avatar s60" = image_tag avatar_icon_for_user(@user, 60), class: "avatar s60"
%li %li
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
.card .card
.card-header .card-header
Account: Account:
%ul.well-list %ul.content-list
%li %li
%span.light Name: %span.light Name:
%strong= @user.name %strong= @user.name
......
...@@ -14,7 +14,7 @@ ...@@ -14,7 +14,7 @@
- if event.push_with_commits? - if event.push_with_commits?
.event-body .event-body
%ul.well-list.event_commits %ul.content-list.event_commits
= render "events/commit", project: project, event: event = 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) - create_mr = event.new_ref? && create_mr_button?(project.default_branch, event.ref_name, project) && event.authored_by?(current_user)
......
- breadcrumb_title "General Settings" - breadcrumb_title "General Settings"
- @content_class = "limit-container-width" unless fluid_layout - @content_class = "limit-container-width" unless fluid_layout
- expanded = Rails.env.test?
.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 %section.settings.gs-general.no-animate#js-general-settings{ class: ('expanded' if expanded) }
.offset-sm-2.col-sm-10 .settings-header
.avatar-container.s160 %h4
= group_icon(@group, alt: '', class: 'avatar group-avatar s160') = _('General')
%p.light %button.btn.js-settings-toggle{ type: 'button' }
- if @group.avatar? = expanded ? _('Collapse') : _('Expand')
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 %p
Removing group will cause all child projects and resources to be removed. = _('Update your group name, description, avatar, and other general settings.')
%br .settings-content
%strong Removed group can not be restored! = render 'groups/settings/general'
.form-actions %section.settings.gs-permissions.no-animate#js-permissions-settings{ class: ('expanded' if expanded) }
= button_to 'Remove group', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_group_message(@group) } .settings-header
%h4
- if supports_nested_groups? = _('Permissions')
.card.bg-warning %button.btn.js-settings-toggle{ type: 'button' }
.card-header Transfer group = expanded ? _('Collapse') : _('Expand')
.card-body %p
= form_for @group, url: transfer_group_path(@group), method: :put do |f| = _('Enable or disable certain group features and choose access levels.')
.form-group .settings-content
= 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) } }) = render 'groups/settings/permissions'
= hidden_field_tag 'new_parent_group_id'
%section.settings.gs-advanced.no-animate#js-advanced-settings{ class: ('expanded' if expanded) }
%ul .settings-header
%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'}. %h4
%li You can only transfer the group to a group you manage. = _('Advanced')
%li You will need to update your local repositories to point to the new location. %button.btn.js-settings-toggle{ type: 'button' }
%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. = expanded ? _('Collapse') : _('Expand')
= f.submit 'Transfer group', class: "btn btn-warning" %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 = render 'shared/confirm_modal', phrase: @group.path
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
.controls .controls
= link_to new_project_path(namespace_id: @group.id), class: "btn btn-sm btn-success" do = link_to new_project_path(namespace_id: @group.id), class: "btn btn-sm btn-success" do
New project New project
%ul.well-list %ul.content-list
- @projects.each do |project| - @projects.each do |project|
%li %li
.list-item-name .list-item-name
......
...@@ -8,13 +8,13 @@ ...@@ -8,13 +8,13 @@
= link_to edit_group_runner_path(@group, runner) do = link_to edit_group_runner_path(@group, runner) do
= icon('edit') = icon('edit')
.pull-right .float-right
- if runner.active? - 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?") } = link_to _('Pause'), pause_group_runner_path(@group, runner), method: :post, class: 'btn btn-sm btn-danger', data: { confirm: _("Are you sure?") }
- else - else
= link_to _('Resume'), resume_group_runner_path(@group, runner), method: :post, class: 'btn btn-success btn-sm' = 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' = 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 %small.light
\##{runner.id} \##{runner.id}
- if runner.description.present? - 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 @@ ...@@ -6,7 +6,7 @@
%section.settings#secret-variables.no-animate{ class: ('expanded' if expanded) } %section.settings#secret-variables.no-animate{ class: ('expanded' if expanded) }
.settings-header .settings-header
%h4 %h4
= _('Secret variables') = _('Variables')
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer' = 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" } %button.btn.btn-default.js-settings-toggle{ type: "button" }
= expanded ? _('Collapse') : _('Expand') = expanded ? _('Collapse') : _('Expand')
......
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
.card .card
.card-header .card-header
Quick help Quick help
%ul.well-list %ul.content-list
%li= link_to 'See our website for getting help', support_url %li= link_to 'See our website for getting help', support_url
%li %li
%button.btn-blank.btn-link.js-trigger-search-bar{ type: 'button' } %button.btn-blank.btn-link.js-trigger-search-bar{ type: 'button' }
......
...@@ -116,9 +116,9 @@ ...@@ -116,9 +116,9 @@
.lead .lead
List with hover effect List with hover effect
%code .well-list %code .hover-list
.example .example
%ul.well-list %ul.hover-list
%li %li
One item One item
%li %li
...@@ -131,7 +131,7 @@ ...@@ -131,7 +131,7 @@
.example .example
.card .card
.card-header Your list .card-header Your list
%ul.well-list %ul.content-list
%li %li
One item One item
%li %li
......
%h5.prepend-top-0 %h5.prepend-top-0
History of authentications History of authentications
%ul.well-list %ul.content-list
- events.each do |event| - events.each do |event|
%li %li
%span.description %span.description
......
- is_current_session = active_session.current?(session) - is_current_session = active_session.current?(session)
%li.list-group-item %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) = active_session_device_type_icon(active_session)
.description.pull-left .description.float-left
%div %div
%strong= active_session.ip_address %strong= active_session.ip_address
- if is_current_session - if is_current_session
...@@ -25,7 +25,7 @@ ...@@ -25,7 +25,7 @@
= l(active_session.created_at, format: :short) = l(active_session.created_at, format: :short)
- unless is_current_session - 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 = 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 %span.sr-only Revoke
Revoke Revoke
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
Your Public Email will be displayed on your public profile. Your Public Email will be displayed on your public profile.
%li %li
All email addresses will be used to identify your commits. All email addresses will be used to identify your commits.
%ul.well-list %ul.content-list
%li %li
= render partial: 'shared/email_with_badge', locals: { email: @primary_email, verified: current_user.confirmed? } = render partial: 'shared/email_with_badge', locals: { email: @primary_email, verified: current_user.confirmed? }
%span.float-right %span.float-right
......
- is_admin = local_assigns.fetch(:admin, false) - is_admin = local_assigns.fetch(:admin, false)
- if @gpg_keys.any? - if @gpg_keys.any?
%ul.well-list %ul.content-list
= render partial: 'profiles/gpg_keys/key', collection: @gpg_keys, locals: { is_admin: is_admin } = render partial: 'profiles/gpg_keys/key', collection: @gpg_keys, locals: { is_admin: is_admin }
- else - else
%p.settings-message.text-center %p.settings-message.text-center
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.card .card
.card-header .card-header
SSH Key SSH Key
%ul.well-list %ul.content-list
%li %li
%span.light Title: %span.light Title:
%strong= @key.title %strong= @key.title
......
- is_admin = local_assigns.fetch(:admin, false) - is_admin = local_assigns.fetch(:admin, false)
- if @keys.any? - if @keys.any?
%ul.well-list %ul.content-list
= render partial: 'profiles/keys/key', collection: @keys, locals: { is_admin: is_admin } = render partial: 'profiles/keys/key', collection: @keys, locals: { is_admin: is_admin }
- else - else
%p.settings-message.text-center %p.settings-message.text-center
......
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
= s_('Branches|Cant find HEAD commit for this branch') = s_('Branches|Cant find HEAD commit for this branch')
- if branch.name != @repository.root_ref - 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, default_branch: @repository.root_ref,
number_commits_ahead: diverging_count_label(number_commits_ahead) } } number_commits_ahead: diverging_count_label(number_commits_ahead) } }
.graph-side .graph-side
...@@ -39,7 +39,7 @@ ...@@ -39,7 +39,7 @@
.bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" } .bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" }
%span.count.count-ahead= diverging_count_label(number_commits_ahead) %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) - 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 = link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-default' do
= _('Merge request') = _('Merge request')
......
= javascript_include_tag 'https://apis.google.com/js/api.js'
%p %p
- link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer') - 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} = 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_errors(@cluster)
.form-group .form-group
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name') = field.label :name, s_('ClusterIntegration|Kubernetes cluster name')
...@@ -14,13 +16,25 @@ ...@@ -14,13 +16,25 @@
= field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field| = field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field|
.form-group .form-group
= provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID') = 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') .js-gcp-project-id-dropdown-entry-point{ data: { docsUrl: 'https://console.cloud.google.com/home/dashboard' } }
= provider_gcp_field.text_field :gcp_project_id, class: 'form-control', placeholder: s_('ClusterIntegration|Project ID') = 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 .form-group
= provider_gcp_field.label :zone, s_('ClusterIntegration|Zone') = 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') = 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 .form-group
= provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes') = provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes')
...@@ -28,8 +42,13 @@ ...@@ -28,8 +42,13 @@
.form-group .form-group
= provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type') = 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') .js-gcp-machine-type-dropdown-entry-point
= provider_gcp_field.text_field :machine_type, class: 'form-control', placeholder: 'n1-standard-4' = 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 .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 @@ ...@@ -15,7 +15,7 @@
%h5.prepend-top-default %h5.prepend-top-default
Webhooks (#{@hooks.count}) Webhooks (#{@hooks.count})
- if @hooks.any? - if @hooks.any?
%ul.well-list %ul.content-list
- @hooks.each do |hook| - @hooks.each do |hook|
= render 'project_hook', hook: hook = render 'project_hook', hook: hook
- else - else
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
%span= time_ago_with_tooltip @build.artifacts_expire_at %span= time_ago_with_tooltip @build.artifacts_expire_at
- if @build.artifacts? - 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) - 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 = link_to keep_project_job_artifacts_path(@project, @build), class: 'btn btn-sm btn-default', method: :post do
Keep Keep
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
- if @build.trigger_variables.any? - if @build.trigger_variables.any?
%p %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 %dl.js-build-variables.trigger-build-variables.hide
- @build.trigger_variables.each do |trigger_variable| - @build.trigger_variables.each do |trigger_variable|
......
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
#{h(@remote_mirror.last_error.strip)} #{h(@remote_mirror.last_error.strip)}
= f.fields_for :remote_mirrors, @remote_mirror do |rm_form| = f.fields_for :remote_mirrors, @remote_mirror do |rm_form|
.form-group .form-group
= rm_form.check_box :enabled, class: "pull-left" = rm_form.check_box :enabled, class: "float-left"
.prepend-left-20 .prepend-left-20
= rm_form.label :enabled, "Remote mirror repository", class: "label-light append-bottom-0" = rm_form.label :enabled, "Remote mirror repository", class: "label-light append-bottom-0"
%p.light.append-bottom-0 %p.light.append-bottom-0
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
= render "projects/mirrors/instructions" = render "projects/mirrors/instructions"
.form-group .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 .prepend-left-20
= rm_form.label :only_protected_branches, class: 'label-light' = rm_form.label :only_protected_branches, class: 'label-light'
= link_to icon('question-circle'), help_page_path('user/project/protected_branches') = link_to icon('question-circle'), help_page_path('user/project/protected_branches')
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.card .card
.card-header .card-header
Domains (#{@domains.count}) 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| - @domains.each do |domain|
%li.pages-domain-list-item.unstyled %li.pages-domain-list-item.unstyled
- if verification_enabled - 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