summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorShinya Maeda <shinya@gitlab.com>2018-05-30 14:50:09 +0900
committerShinya Maeda <shinya@gitlab.com>2018-05-30 14:50:09 +0900
commit09122f93c34b15cb827aabdbdf35fc33b08f93af (patch)
tree57c137ef57621a7a2ed4940c56c7f5cbe6ec1c80 /app
parent1d20679e9c8b1ba16bebaf982255946e7207b4d4 (diff)
parent5b1416aa74c4fa80e0c324fd2907166af5ca479b (diff)
downloadgitlab-ce-09122f93c34b15cb827aabdbdf35fc33b08f93af.tar.gz
Merge branch 'master' into per-project-pipeline-iid
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue4
-rw-r--r--app/assets/javascripts/ide/components/changed_file_icon.vue2
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue6
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/message_field.vue2
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue26
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue2
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_details_block.vue10
-rw-r--r--app/assets/javascripts/merge_request_tabs.js2
-rw-r--r--app/assets/javascripts/pages/groups/edit/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/clusters/gcp/new/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue2
-rw-r--r--app/assets/javascripts/pages/projects/wikis/wikis.js9
-rw-r--r--app/assets/javascripts/pages/users/user_tabs.js31
-rw-r--r--app/assets/javascripts/profile/account/components/update_username.vue2
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js71
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue142
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue201
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue116
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/constants.js11
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/index.js88
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js95
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js3
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/store/index.js18
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/store/mutation_types.js8
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/store/mutations.js28
-rw-r--r--app/assets/javascripts/projects/gke_cluster_dropdowns/store/state.js13
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue55
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue)6
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue46
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue4
-rw-r--r--app/assets/stylesheets/bootstrap_migration.scss8
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss4
-rw-r--r--app/assets/stylesheets/framework/lists.scss2
-rw-r--r--app/assets/stylesheets/pages/profile.scss2
-rw-r--r--app/assets/stylesheets/pages/repo.scss8
-rw-r--r--app/controllers/admin/dashboard_controller.rb4
-rw-r--r--app/controllers/profiles_controller.rb2
-rw-r--r--app/controllers/projects/clusters/gcp_controller.rb32
-rw-r--r--app/controllers/projects/clusters_controller.rb13
-rw-r--r--app/controllers/projects/environments_controller.rb10
-rw-r--r--app/controllers/projects/wikis_controller.rb8
-rw-r--r--app/finders/group_projects_finder.rb26
-rw-r--r--app/helpers/application_settings_helper.rb2
-rw-r--r--app/helpers/count_helper.rb8
-rw-r--r--app/helpers/nav_helper.rb1
-rw-r--r--app/helpers/projects_helper.rb3
-rw-r--r--app/models/application_setting.rb39
-rw-r--r--app/models/concerns/batch_destroy_dependent_associations.rb28
-rw-r--r--app/models/concerns/diff_file.rb9
-rw-r--r--app/models/diff_note.rb67
-rw-r--r--app/models/event.rb2
-rw-r--r--app/models/internal_id.rb24
-rw-r--r--app/models/merge_request_diff_file.rb7
-rw-r--r--app/models/note.rb4
-rw-r--r--app/models/note_diff_file.rb7
-rw-r--r--app/models/project.rb1
-rw-r--r--app/services/application_settings/update_service.rb12
-rw-r--r--app/services/check_gcp_project_billing_service.rb11
-rw-r--r--app/services/projects/destroy_service.rb8
-rw-r--r--app/uploaders/object_storage.rb2
-rw-r--r--app/views/admin/application_settings/_performance_bar.html.haml4
-rw-r--r--app/views/admin/application_settings/_repository_mirrors_form.html.haml2
-rw-r--r--app/views/admin/application_settings/_signin.html.haml2
-rw-r--r--app/views/admin/application_settings/_terms.html.haml4
-rw-r--r--app/views/admin/application_settings/_visibility_and_access.html.haml2
-rw-r--r--app/views/admin/dashboard/index.html.haml20
-rw-r--r--app/views/admin/groups/show.html.haml8
-rw-r--r--app/views/admin/projects/show.html.haml6
-rw-r--r--app/views/admin/users/_profile.html.haml2
-rw-r--r--app/views/admin/users/projects.html.haml4
-rw-r--r--app/views/admin/users/show.html.haml4
-rw-r--r--app/views/events/event/_push.html.haml2
-rw-r--r--app/views/groups/edit.html.haml109
-rw-r--r--app/views/groups/projects.html.haml2
-rw-r--r--app/views/groups/runners/_runner.html.haml4
-rw-r--r--app/views/groups/settings/_advanced.html.haml49
-rw-r--r--app/views/groups/settings/_general.html.haml38
-rw-r--r--app/views/groups/settings/_permissions.html.haml28
-rw-r--r--app/views/groups/settings/ci_cd/show.html.haml2
-rw-r--r--app/views/help/index.html.haml2
-rw-r--r--app/views/help/ui.html.haml6
-rw-r--r--app/views/profiles/_event_table.html.haml2
-rw-r--r--app/views/profiles/active_sessions/_active_session.html.haml6
-rw-r--r--app/views/profiles/emails/index.html.haml2
-rw-r--r--app/views/profiles/gpg_keys/_key_table.html.haml2
-rw-r--r--app/views/profiles/keys/_key_details.html.haml2
-rw-r--r--app/views/profiles/keys/_key_table.html.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml4
-rw-r--r--app/views/projects/clusters/gcp/_form.html.haml33
-rw-r--r--app/views/projects/hooks/_index.html.haml2
-rw-r--r--app/views/projects/jobs/_sidebar.html.haml4
-rw-r--r--app/views/projects/mirrors/_push.html.haml4
-rw-r--r--app/views/projects/pages/_list.html.haml2
-rw-r--r--app/views/projects/pipelines/new.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/_autodevops_form.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml2
-rw-r--r--app/views/projects/wikis/empty.html.haml6
-rw-r--r--app/views/shared/_email_with_badge.html.haml2
-rw-r--r--app/views/shared/_visibility_level.html.haml2
-rw-r--r--app/views/shared/empty_states/_wikis.html.haml30
-rw-r--r--app/views/shared/empty_states/_wikis_layout.html.haml7
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/views/shared/milestones/_issuables.html.haml2
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml4
-rw-r--r--app/views/shared/milestones/_top.html.haml2
-rw-r--r--app/views/shared/snippets/_snippet.html.haml2
-rw-r--r--app/views/sherlock/queries/_backtrace.html.haml4
-rw-r--r--app/views/sherlock/queries/_general.html.haml6
-rw-r--r--app/views/sherlock/transactions/_general.html.haml2
-rw-r--r--app/views/users/terms/index.html.haml4
-rw-r--r--app/workers/all_queues.yml2
-rw-r--r--app/workers/check_gcp_project_billing_worker.rb92
-rw-r--r--app/workers/create_note_diff_file_worker.rb9
114 files changed, 1453 insertions, 466 deletions
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
index ae942b2c1a7..5975cb9669e 100644
--- a/app/assets/javascripts/badges/components/badge_form.vue
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -160,7 +160,7 @@ export default {
@input="debouncedPreview"
/>
<span
- class="help-block"
+ class="form-text text-muted"
v-html="helpText"
></span>
</div>
@@ -176,7 +176,7 @@ export default {
@input="debouncedPreview"
/>
<span
- class="help-block"
+ class="form-text text-muted"
v-html="helpText"
></span>
</div>
diff --git a/app/assets/javascripts/ide/components/changed_file_icon.vue b/app/assets/javascripts/ide/components/changed_file_icon.vue
index 1cec84706fc..a4e06bbbe3c 100644
--- a/app/assets/javascripts/ide/components/changed_file_icon.vue
+++ b/app/assets/javascripts/ide/components/changed_file_icon.vue
@@ -43,7 +43,7 @@ export default {
return `${this.changedIcon}-solid`;
},
changedIconClass() {
- return `multi-${this.changedIcon} pull-left`;
+ return `multi-${this.changedIcon} float-left`;
},
tooltipTitle() {
if (!this.showTooltip) return undefined;
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
index 81961fe3c57..705953c86e3 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -144,14 +144,14 @@ export default {
<loading-button
:loading="submitCommitLoading"
:disabled="commitButtonDisabled"
- container-class="btn btn-success btn-sm pull-left"
+ container-class="btn btn-success btn-sm float-left"
:label="__('Commit')"
@click="commitChanges"
/>
<button
v-if="!discardDraftButtonDisabled"
type="button"
- class="btn btn-default btn-sm pull-right"
+ class="btn btn-default btn-sm float-right"
@click="discardDraft"
>
{{ __('Discard draft') }}
@@ -159,7 +159,7 @@ export default {
<button
v-else
type="button"
- class="btn btn-default btn-sm pull-right"
+ class="btn btn-default btn-sm float-right"
@click="toggleIsSmall"
>
{{ __('Collapse') }}
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
index c3ac18bfb83..1325fc993b2 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -120,7 +120,7 @@ export default {
</ul>
<p
v-else
- class="multi-file-commit-list help-block"
+ class="multi-file-commit-list form-text text-muted"
>
{{ __('No changes') }}
</p>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
index dcd934f76b7..f14fcdc88ed 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
@@ -80,7 +80,7 @@ export default {
{{ __('Commit Message') }}
<span
v-popover="$options.popoverOptions"
- class="help-block prepend-left-10"
+ class="form-text text-muted prepend-left-10"
>
<icon
name="question"
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index d83a90f71e1..dd2800179ff 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -72,21 +72,19 @@ export default {
<form
slot="body"
@submit.prevent="createEntryInStore"
- class="form-group row append-bottom-0"
+ class="form-group row"
>
- <fieldset class="form-group append-bottom-0">
- <label class="label-light col-form-label col-sm-3 ide-new-modal-label">
- {{ __('Name') }}
- </label>
- <div class="col-sm-9">
- <input
- type="text"
- class="form-control"
- v-model="entryName"
- ref="fieldName"
- />
- </div>
- </fieldset>
+ <label class="label-light col-form-label col-sm-3">
+ {{ __('Name') }}
+ </label>
+ <div class="col-sm-9">
+ <input
+ type="text"
+ class="form-control"
+ v-model="entryName"
+ ref="fieldName"
+ />
+ </div>
</form>
</deprecated-modal>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index 442697e1c80..f56aeced806 100644
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -169,7 +169,7 @@ export default {
:show-tooltip="true"
:show-staged-icon="true"
:force-modified-icon="true"
- class="pull-right"
+ class="float-right"
/>
</span>
<new-dropdown
diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
index 82fec7e936b..8f3c66b0cbe 100644
--- a/app/assets/javascripts/jobs/components/sidebar_details_block.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
@@ -48,11 +48,10 @@ export default {
return `${this.job.runner.description} (#${this.job.runner.id})`;
},
retryButtonClass() {
- let className = 'js-retry-button pull-right btn btn-retry d-none d-md-block d-lg-block d-xl-block';
+ let className =
+ 'js-retry-button float-right btn btn-retry d-none d-md-block d-lg-block d-xl-block';
className +=
- this.job.status && this.job.recoverable
- ? ' btn-primary'
- : ' btn-inverted-secondary';
+ this.job.status && this.job.recoverable ? ' btn-primary' : ' btn-inverted-secondary';
return className;
},
hasTimeout() {
@@ -104,8 +103,7 @@ export default {
<button
type="button"
:aria-label="__('Toggle Sidebar')"
- class="btn btn-blank gutter-toggle pull-right
- d-block d-sm-block d-md-none js-sidebar-build-toggle"
+ class="btn btn-blank gutter-toggle float-right d-block d-md-none js-sidebar-build-toggle"
>
<i
aria-hidden="true"
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 3548c07aea8..bac7d966ecc 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -362,7 +362,7 @@ export default class MergeRequestTabs {
//
// status - Boolean, true to show, false to hide
toggleLoading(status) {
- $('.mr-loading-status .loading').toggleClass('hidden', status);
+ $('.mr-loading-status .loading').toggleClass('hidden', !status);
}
diffViewType() {
diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js
index bb91ac84ffb..8737f537296 100644
--- a/app/assets/javascripts/pages/groups/edit/index.js
+++ b/app/assets/javascripts/pages/groups/edit/index.js
@@ -1,9 +1,15 @@
import groupAvatar from '~/group_avatar';
import TransferDropdown from '~/groups/transfer_dropdown';
import initConfirmDangerModal from '~/confirm_danger_modal';
+import initSettingsPanels from '~/settings_panels';
document.addEventListener('DOMContentLoaded', () => {
groupAvatar();
new TransferDropdown(); // eslint-disable-line no-new
initConfirmDangerModal();
});
+
+document.addEventListener('DOMContentLoaded', () => {
+ // Initialize expandable settings panels
+ initSettingsPanels();
+});
diff --git a/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js b/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js
new file mode 100644
index 00000000000..d4f34e32a48
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/clusters/gcp/new/index.js
@@ -0,0 +1,5 @@
+import initGkeDropdowns from '~/projects/gke_cluster_dropdowns';
+
+document.addEventListener('DOMContentLoaded', () => {
+ initGkeDropdowns();
+});
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index 755a34b7348..06b0ab184ed 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -213,7 +213,7 @@
</i>
</div>
</div>
- <span class="help-block">{{ visibilityLevelDescription }}</span>
+ <span class="form-text text-muted">{{ visibilityLevelDescription }}</span>
<label
v-if="visibilityLevel !== visibilityOptions.PRIVATE"
class="request-access"
diff --git a/app/assets/javascripts/pages/projects/wikis/wikis.js b/app/assets/javascripts/pages/projects/wikis/wikis.js
index 34a12ef76a1..dcd0b9a76ce 100644
--- a/app/assets/javascripts/pages/projects/wikis/wikis.js
+++ b/app/assets/javascripts/pages/projects/wikis/wikis.js
@@ -1,5 +1,7 @@
import bp from '../../../breakpoints';
import { slugify } from '../../../lib/utils/text_utility';
+import { parseQueryStringIntoObject } from '../../../lib/utils/common_utils';
+import { mergeUrlParams, redirectTo } from '../../../lib/utils/url_utility';
export default class Wikis {
constructor() {
@@ -28,7 +30,12 @@ export default class Wikis {
if (slug.length > 0) {
const wikisPath = slugInput.getAttribute('data-wikis-path');
- window.location.href = `${wikisPath}/${slug}`;
+
+ // If the wiki is empty, we need to merge the current URL params to keep the "create" view.
+ const params = parseQueryStringIntoObject(window.location.search.substr(1));
+ const url = mergeUrlParams(params, `${wikisPath}/${slug}`);
+ redirectTo(url);
+
e.preventDefault();
}
}
diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index ca375007ec5..9404b06615e 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -77,10 +77,9 @@ export default class UserTabs {
this.action = action || this.defaultAction;
this.$parentEl = $(parentEl) || $(document);
this.windowLocation = window.location;
- this.$parentEl.find('.nav-links a')
- .each((i, navLink) => {
- this.loaded[$(navLink).attr('data-action')] = false;
- });
+ this.$parentEl.find('.nav-links a').each((i, navLink) => {
+ this.loaded[$(navLink).attr('data-action')] = false;
+ });
this.actions = Object.keys(this.loaded);
this.bindEvents();
@@ -116,8 +115,7 @@ export default class UserTabs {
}
activateTab(action) {
- return this.$parentEl.find(`.nav-links .js-${action}-tab a`)
- .tab('show');
+ return this.$parentEl.find(`.nav-links .js-${action}-tab a`).tab('show');
}
setTab(action, endpoint) {
@@ -137,7 +135,8 @@ export default class UserTabs {
loadTab(action, endpoint) {
this.toggleLoading(true);
- return axios.get(endpoint)
+ return axios
+ .get(endpoint)
.then(({ data }) => {
const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html);
@@ -161,10 +160,11 @@ export default class UserTabs {
const utcOffset = $calendarWrap.data('utcOffset');
let utcFormatted = 'UTC';
if (utcOffset !== 0) {
- utcFormatted = `UTC${utcOffset > 0 ? '+' : ''}${(utcOffset / 3600)}`;
+ utcFormatted = `UTC${utcOffset > 0 ? '+' : ''}${utcOffset / 3600}`;
}
- axios.get(calendarPath)
+ axios
+ .get(calendarPath)
.then(({ data }) => {
$calendarWrap.html(CALENDAR_TEMPLATE);
$calendarWrap.find('.calendar-hint').append(`(Timezone: ${utcFormatted})`);
@@ -180,17 +180,20 @@ export default class UserTabs {
}
toggleLoading(status) {
- return this.$parentEl.find('.loading-status .loading')
- .toggleClass('hidden', status);
+ return this.$parentEl.find('.loading-status .loading').toggleClass('hidden', !status);
}
setCurrentAction(source) {
let newState = source;
newState = newState.replace(/\/+$/, '');
newState += this.windowLocation.search + this.windowLocation.hash;
- history.replaceState({
- url: newState,
- }, document.title, newState);
+ history.replaceState(
+ {
+ url: newState,
+ },
+ document.title,
+ newState,
+ );
return newState;
}
diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue
index a7a2a7235fd..b37febe523c 100644
--- a/app/assets/javascripts/profile/account/components/update_username.vue
+++ b/app/assets/javascripts/profile/account/components/update_username.vue
@@ -99,7 +99,7 @@ Please update your Git repository remotes as soon as possible.`),
:disabled="isRequestPending"
/>
</div>
- <p class="help-block">
+ <p class="form-text text-muted">
{{ path }}
</p>
</div>
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js
new file mode 100644
index 00000000000..c15d8ba49e1
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_dropdown_mixin.js
@@ -0,0 +1,71 @@
+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;
+ }
+ },
+ },
+};
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue
new file mode 100644
index 00000000000..ab7d2d41ece
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue
@@ -0,0 +1,142 @@
+<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>
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue
new file mode 100644
index 00000000000..25350ef0fa9
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue
@@ -0,0 +1,201 @@
+<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>
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue
new file mode 100644
index 00000000000..8ee4eefcd91
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue
@@ -0,0 +1,116 @@
+<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>
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/constants.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/constants.js
new file mode 100644
index 00000000000..2a1c0819916
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/constants.js
@@ -0,0 +1,11 @@
+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';
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/index.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/index.js
new file mode 100644
index 00000000000..729b9404b64
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/index.js
@@ -0,0 +1,88 @@
+/* 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;
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js
new file mode 100644
index 00000000000..4834a856271
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/actions.js
@@ -0,0 +1,95 @@
+/* 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 () => {};
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js
new file mode 100644
index 00000000000..e39f02d0894
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/getters.js
@@ -0,0 +1,3 @@
+export const hasProject = state => !!state.selectedProject.projectId;
+export const hasZone = state => !!state.selectedZone;
+export const hasMachineType = state => !!state.selectedMachineType;
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/index.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/index.js
new file mode 100644
index 00000000000..5f72060633e
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/index.js
@@ -0,0 +1,18 @@
+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();
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/mutation_types.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/mutation_types.js
new file mode 100644
index 00000000000..45a91efc2d9
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/mutation_types.js
@@ -0,0 +1,8 @@
+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';
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/mutations.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/mutations.js
new file mode 100644
index 00000000000..88a2c1b630d
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/mutations.js
@@ -0,0 +1,28 @@
+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 });
+ },
+};
diff --git a/app/assets/javascripts/projects/gke_cluster_dropdowns/store/state.js b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/state.js
new file mode 100644
index 00000000000..9f3c473d4bc
--- /dev/null
+++ b/app/assets/javascripts/projects/gke_cluster_dropdowns/store/state.js
@@ -0,0 +1,13 @@
+export default () => ({
+ selectedProject: {
+ projectId: '',
+ name: '',
+ },
+ selectedZone: '',
+ selectedMachineType: '',
+ isValidatingProjectBilling: null,
+ projectHasBillingEnabled: null,
+ projects: [],
+ zones: [],
+ machineTypes: [],
+});
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
new file mode 100644
index 00000000000..c159333d89a
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
@@ -0,0 +1,55 @@
+<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>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue
index 1832c3c1757..1fe27eb97ab 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_hidden_input.vue
@@ -5,8 +5,8 @@ export default {
type: String,
required: true,
},
- label: {
- type: Object,
+ value: {
+ type: [Number, String],
required: true,
},
},
@@ -17,6 +17,6 @@ export default {
<input
type="hidden"
:name="name"
- :value="label.id"
+ :value="value"
/>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue
new file mode 100644
index 00000000000..c2145a26e64
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_search_input.vue
@@ -0,0 +1,46 @@
+<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>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
index 70b46a9c2bb..f155ac2be02 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
@@ -2,13 +2,13 @@
import $ from 'jquery';
import { __ } from '~/locale';
import LabelsSelect from '~/labels_select';
+import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import LoadingIcon from '../../loading_icon.vue';
import DropdownTitle from './dropdown_title.vue';
import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
import DropdownButton from './dropdown_button.vue';
-import DropdownHiddenInput from './dropdown_hidden_input.vue';
import DropdownHeader from './dropdown_header.vue';
import DropdownSearchInput from './dropdown_search_input.vue';
import DropdownFooter from './dropdown_footer.vue';
@@ -140,7 +140,7 @@ export default {
v-for="label in context.labels"
:key="label.id"
:name="hiddenInputName"
- :label="label"
+ :value="label.id"
/>
<div
class="dropdown"
diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
index 3b7ee5c73e6..e1a47f3d686 100644
--- a/app/assets/stylesheets/bootstrap_migration.scss
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -131,6 +131,10 @@ table {
}
.card {
+ .card-title {
+ margin-bottom: 0;
+ }
+
&.card-without-border {
@extend .border-0;
}
@@ -147,3 +151,7 @@ table {
.nav-tabs .nav-link {
border: 0;
}
+
+pre code {
+ white-space: pre-wrap;
+}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 1570b1f2eaa..b91d579cae6 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -63,6 +63,10 @@
border-radius: $border-radius-base;
white-space: nowrap;
+ &:disabled.read-only {
+ color: $gl-text-color !important;
+ }
+
&.no-outline {
outline: 0;
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 17f4958d535..d54490c87c6 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -2,7 +2,7 @@
* Well styled list
*
*/
-.card-body-list {
+.hover-list {
position: relative;
margin: 0;
padding: 0;
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 06078f1d12e..5d0d59e12f2 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -405,7 +405,7 @@ table.u2f-registrations {
margin-right: $gl-padding / 4;
}
- .label-verification-status {
+ .badge-verification-status {
border-width: 1px;
border-style: solid;
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 17d7087bd85..d5c6048037a 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -546,7 +546,7 @@
margin-right: 0;
}
- &.help-block {
+ &.form-text.text-muted {
margin-left: 0;
right: 0;
}
@@ -952,7 +952,7 @@
height: 30px;
}
- .help-block {
+ .form-text.text-muted {
margin-top: 2px;
color: $blue-500;
cursor: pointer;
@@ -1088,10 +1088,6 @@
font-size: 12px;
}
-.ide-new-modal-label {
- line-height: 34px;
-}
-
.multi-file-commit-panel-success-message {
position: absolute;
top: 61px;
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index d6a6bc7d4a1..737942f3eb2 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -1,7 +1,11 @@
class Admin::DashboardController < Admin::ApplicationController
include CountHelper
+ COUNTED_ITEMS = [Project, User, Group, ForkedProjectLink, Issue, MergeRequest,
+ Note, Snippet, Key, Milestone].freeze
+
def index
+ @counts = Gitlab::Database::Count.approximate_counts(COUNTED_ITEMS)
@projects = Project.order_id_desc.without_deleted.with_route.limit(10)
@users = User.order_id_desc.limit(10)
@groups = Group.order_id_desc.with_route.limit(10)
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index ac71f72e624..9f5ad23a20f 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -93,8 +93,6 @@ class ProfilesController < Profiles::ApplicationController
:linkedin,
:location,
:name,
- :password,
- :password_confirmation,
:public_email,
:skype,
:twitter,
diff --git a/app/controllers/projects/clusters/gcp_controller.rb b/app/controllers/projects/clusters/gcp_controller.rb
index 6b0b22f8e73..c2c5ad61e01 100644
--- a/app/controllers/projects/clusters/gcp_controller.rb
+++ b/app/controllers/projects/clusters/gcp_controller.rb
@@ -1,9 +1,8 @@
class Projects::Clusters::GcpController < Projects::ApplicationController
before_action :authorize_read_cluster!
- before_action :authorize_google_api, except: [:login]
- before_action :authorize_google_project_billing, only: [:new, :create]
before_action :authorize_create_cluster!, only: [:new, :create]
- before_action :verify_billing, only: [:create]
+ before_action :authorize_google_api, except: :login
+ helper_method :token_in_session
def login
begin
@@ -37,21 +36,6 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
private
- def verify_billing
- case google_project_billing_status
- when nil
- flash.now[:alert] = _('We could not verify that one of your projects on GCP has billing enabled. Please try again.')
- when false
- flash.now[:alert] = _('Please <a href=%{link_to_billing} target="_blank" rel="noopener noreferrer">enable billing for one of your projects to be able to create a Kubernetes cluster</a>, then try again.').html_safe % { link_to_billing: "https://console.cloud.google.com/freetrial?utm_campaign=2018_cpanel&utm_source=gitlab&utm_medium=referral" }
- when true
- return
- end
-
- @cluster = ::Clusters::Cluster.new(create_params)
-
- render :new
- end
-
def create_params
params.require(:cluster).permit(
:enabled,
@@ -75,18 +59,8 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
end
end
- def authorize_google_project_billing
- redis_token_key = CheckGcpProjectBillingWorker.store_session_token(token_in_session)
- CheckGcpProjectBillingWorker.perform_async(redis_token_key)
- end
-
- def google_project_billing_status
- CheckGcpProjectBillingWorker.get_billing_state(token_in_session)
- end
-
def token_in_session
- @token_in_session ||=
- session[GoogleApi::CloudPlatform::Client.session_key_for_token]
+ session[GoogleApi::CloudPlatform::Client.session_key_for_token]
end
def expires_at_in_session
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index aeaba3a0acf..d58039b7d42 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -71,19 +71,6 @@ class Projects::ClustersController < Projects::ApplicationController
.present(current_user: current_user)
end
- def create_params
- params.require(:cluster).permit(
- :enabled,
- :name,
- :provider_type,
- provider_gcp_attributes: [
- :gcp_project_id,
- :zone,
- :num_nodes,
- :machine_type
- ])
- end
-
def update_params
if cluster.managed?
params.require(:cluster).permit(
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 52d528e816e..0821362f5df 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -7,6 +7,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :authorize_admin_environment!, only: [:terminal, :terminal_websocket_authorize]
before_action :environment, only: [:show, :edit, :update, :stop, :terminal, :terminal_websocket_authorize, :metrics]
before_action :verify_api_request!, only: :terminal_websocket_authorize
+ before_action :expire_etag_cache, only: [:index]
def index
@environments = project.environments
@@ -148,6 +149,15 @@ class Projects::EnvironmentsController < Projects::ApplicationController
Gitlab::Workhorse.verify_api_request!(request.headers)
end
+ def expire_etag_cache
+ return if request.format.json?
+
+ # this forces to reload json content
+ Gitlab::EtagCaching::Store.new.tap do |store|
+ store.touch(project_environments_path(project, format: :json))
+ end
+ end
+
def environment_params
params.require(:environment).permit(:name, :external_url)
end
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 1b0751f48c5..242e6491456 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -14,6 +14,8 @@ class Projects::WikisController < Projects::ApplicationController
def show
@page = @project_wiki.find_page(params[:id], params[:version_id])
+ view_param = @project_wiki.empty? ? params[:view] : 'create'
+
if @page
render 'show'
elsif file = @project_wiki.find_file(params[:id], params[:version_id])
@@ -26,12 +28,12 @@ class Projects::WikisController < Projects::ApplicationController
disposition: 'inline',
filename: file.name
)
- else
- return render('empty') unless can?(current_user, :create_wiki, @project)
-
+ elsif can?(current_user, :create_wiki, @project) && view_param == 'create'
@page = build_page(title: params[:id])
render 'edit'
+ else
+ render 'empty'
end
end
diff --git a/app/finders/group_projects_finder.rb b/app/finders/group_projects_finder.rb
index f73cf8adb4d..b6bdb2b7b0f 100644
--- a/app/finders/group_projects_finder.rb
+++ b/app/finders/group_projects_finder.rb
@@ -39,25 +39,15 @@ class GroupProjectsFinder < ProjectsFinder
end
def collection_with_user
- if group.users.include?(current_user)
- if only_shared?
- [shared_projects]
- elsif only_owned?
- [owned_projects]
- else
- [shared_projects, owned_projects]
- end
+ if only_shared?
+ [shared_projects.public_or_visible_to_user(current_user)]
+ elsif only_owned?
+ [owned_projects.public_or_visible_to_user(current_user)]
else
- if only_shared?
- [shared_projects.public_or_visible_to_user(current_user)]
- elsif only_owned?
- [owned_projects.public_or_visible_to_user(current_user)]
- else
- [
- owned_projects.public_or_visible_to_user(current_user),
- shared_projects.public_or_visible_to_user(current_user)
- ]
- end
+ [
+ owned_projects.public_or_visible_to_user(current_user),
+ shared_projects.public_or_visible_to_user(current_user)
+ ]
end
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index b948e431882..adc423af9e1 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -204,7 +204,7 @@ module ApplicationSettingsHelper
:pages_domain_verification_enabled,
:password_authentication_enabled_for_web,
:password_authentication_enabled_for_git,
- :performance_bar_allowed_group_id,
+ :performance_bar_allowed_group_path,
:performance_bar_enabled,
:plantuml_enabled,
:plantuml_url,
diff --git a/app/helpers/count_helper.rb b/app/helpers/count_helper.rb
index 24ee62e68ba..5cd98f40f78 100644
--- a/app/helpers/count_helper.rb
+++ b/app/helpers/count_helper.rb
@@ -1,5 +1,9 @@
module CountHelper
- def approximate_count_with_delimiters(model)
- number_with_delimiter(Gitlab::Database::Count.approximate_count(model))
+ def approximate_count_with_delimiters(count_data, model)
+ count = count_data[model]
+
+ raise "Missing model #{model} from count data" unless count
+
+ number_with_delimiter(count)
end
end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 7754c34d6f0..a84a39235d8 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -11,6 +11,7 @@ module NavHelper
class_name = page_gutter_class
class_name << 'page-with-contextual-sidebar' if defined?(@left_sidebar) && @left_sidebar
class_name << 'page-with-icon-sidebar' if collapsed_sidebar? && @left_sidebar
+ class_name -= ['right-sidebar-expanded'] if defined?(@right_sidebar) && !@right_sidebar
class_name
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index fa54eafd3a3..55078e1a2d2 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -257,6 +257,9 @@ module ProjectsHelper
if project.builds_enabled? && can?(current_user, :read_pipeline, project)
nav_tabs << :pipelines
+ end
+
+ if can?(current_user, :read_environment, project) || can?(current_user, :read_cluster, project)
nav_tabs << :operations
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index e8ccb320fae..b12f7a2c83f 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -230,6 +230,7 @@ class ApplicationSetting < ActiveRecord::Base
after_commit do
reset_memoized_terms
end
+ after_commit :expire_performance_bar_allowed_user_ids_cache, if: -> { previous_changes.key?('performance_bar_allowed_group_id') }
def self.defaults
{
@@ -386,31 +387,6 @@ class ApplicationSetting < ActiveRecord::Base
super(levels.map { |level| Gitlab::VisibilityLevel.level_value(level) })
end
- def performance_bar_allowed_group_id=(group_full_path)
- group_full_path = nil if group_full_path.blank?
-
- if group_full_path.nil?
- if group_full_path != performance_bar_allowed_group_id
- super(group_full_path)
- Gitlab::PerformanceBar.expire_allowed_user_ids_cache
- end
-
- return
- end
-
- group = Group.find_by_full_path(group_full_path)
-
- if group
- if group.id != performance_bar_allowed_group_id
- super(group.id)
- Gitlab::PerformanceBar.expire_allowed_user_ids_cache
- end
- else
- super(nil)
- Gitlab::PerformanceBar.expire_allowed_user_ids_cache
- end
- end
-
def performance_bar_allowed_group
Group.find_by_id(performance_bar_allowed_group_id)
end
@@ -420,15 +396,6 @@ class ApplicationSetting < ActiveRecord::Base
performance_bar_allowed_group_id.present?
end
- # - If `enable` is true, we early return since the actual attribute that holds
- # the enabling/disabling is `performance_bar_allowed_group_id`
- # - If `enable` is false, we set `performance_bar_allowed_group_id` to `nil`
- def performance_bar_enabled=(enable)
- return if Gitlab::Utils.to_boolean(enable)
-
- self.performance_bar_allowed_group_id = nil
- end
-
# Choose one of the available repository storage options. Currently all have
# equal weighting.
def pick_repository_storage
@@ -506,4 +473,8 @@ class ApplicationSetting < ActiveRecord::Base
errors.add(:terms, "You need to set terms to be enforced") unless terms.present?
end
+
+ def expire_performance_bar_allowed_user_ids_cache
+ Gitlab::PerformanceBar.expire_allowed_user_ids_cache
+ end
end
diff --git a/app/models/concerns/batch_destroy_dependent_associations.rb b/app/models/concerns/batch_destroy_dependent_associations.rb
new file mode 100644
index 00000000000..353ee2e73d0
--- /dev/null
+++ b/app/models/concerns/batch_destroy_dependent_associations.rb
@@ -0,0 +1,28 @@
+# 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
diff --git a/app/models/concerns/diff_file.rb b/app/models/concerns/diff_file.rb
new file mode 100644
index 00000000000..72332072012
--- /dev/null
+++ b/app/models/concerns/diff_file.rb
@@ -0,0 +1,9 @@
+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
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 616a626419b..d752d5bcdee 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -3,6 +3,7 @@
# A note of this type can be resolvable.
class DiffNote < Note
include NoteOnDiff
+ include Gitlab::Utils::StrongMemoize
NOTEABLE_TYPES = %w(MergeRequest Commit).freeze
@@ -12,7 +13,6 @@ class DiffNote < Note
validates :original_position, presence: true
validates :position, presence: true
- validates :diff_line, presence: true, if: :on_text?
validates :line_code, presence: true, line_code: true, if: :on_text?
validates :noteable_type, inclusion: { in: NOTEABLE_TYPES }
validate :positions_complete
@@ -23,6 +23,7 @@ class DiffNote < Note
before_validation :update_position, on: :create, if: :on_text?
before_validation :set_line_code, if: :on_text?
after_save :keep_around_commits
+ after_commit :create_diff_file, on: :create
def discussion_class(*)
DiffDiscussion
@@ -53,21 +54,25 @@ class DiffNote < Note
position.position_type == "image"
end
+ def create_diff_file
+ return unless should_create_diff_file?
+
+ diff_file = fetch_diff_file
+ diff_line = diff_file.line_for_position(self.original_position)
+
+ creation_params = diff_file.diff.to_hash
+ .except(:too_large)
+ .merge(diff: diff_file.diff_hunk(diff_line))
+
+ create_note_diff_file(creation_params)
+ end
+
def diff_file
- @diff_file ||=
- begin
- if created_at_diff?(noteable.diff_refs)
- # We're able to use the already persisted diffs (Postgres) if we're
- # presenting a "current version" of the MR discussion diff.
- # So no need to make an extra Gitaly diff request for it.
- # As an extra benefit, the returned `diff_file` already
- # has `highlighted_diff_lines` data set from Redis on
- # `Diff::FileCollection::MergeRequestDiff`.
- noteable.diffs(paths: original_position.paths, expanded: true).diff_files.first
- else
- original_position.diff_file(self.project.repository)
- end
- end
+ strong_memoize(:diff_file) do
+ enqueue_diff_file_creation_job if should_create_diff_file?
+
+ fetch_diff_file
+ end
end
def diff_line
@@ -98,6 +103,38 @@ class DiffNote < Note
private
+ def enqueue_diff_file_creation_job
+ # Avoid enqueuing multiple file creation jobs at once for a note (i.e.
+ # parallel calls to `DiffNote#diff_file`).
+ lease = Gitlab::ExclusiveLease.new("note_diff_file_creation:#{id}", timeout: 1.hour.to_i)
+ return unless lease.try_obtain
+
+ CreateNoteDiffFileWorker.perform_async(id)
+ end
+
+ def should_create_diff_file?
+ on_text? && note_diff_file.nil? && self == discussion.first_note
+ end
+
+ def fetch_diff_file
+ if note_diff_file
+ diff = Gitlab::Git::Diff.new(note_diff_file.to_hash)
+ Gitlab::Diff::File.new(diff,
+ repository: project.repository,
+ diff_refs: original_position.diff_refs)
+ elsif created_at_diff?(noteable.diff_refs)
+ # We're able to use the already persisted diffs (Postgres) if we're
+ # presenting a "current version" of the MR discussion diff.
+ # So no need to make an extra Gitaly diff request for it.
+ # As an extra benefit, the returned `diff_file` already
+ # has `highlighted_diff_lines` data set from Redis on
+ # `Diff::FileCollection::MergeRequestDiff`.
+ noteable.diffs(paths: original_position.paths, expanded: true).diff_files.first
+ else
+ original_position.diff_file(self.project.repository)
+ end
+ end
+
def supported?
for_commit? || self.noteable.has_complete_diff_refs?
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 741a84194e2..ac0b1c7b27c 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -40,6 +40,7 @@ class Event < ActiveRecord::Base
).freeze
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
+ REPOSITORY_UPDATED_AT_INTERVAL = 5.minutes
delegate :name, :email, :public_email, :username, to: :author, prefix: true, allow_nil: true
delegate :title, to: :issue, prefix: true, allow_nil: true
@@ -391,6 +392,7 @@ class Event < ActiveRecord::Base
def set_last_repository_updated_at
Project.unscoped.where(id: project_id)
+ .where("last_repository_updated_at < ? OR last_repository_updated_at IS NULL", REPOSITORY_UPDATED_AT_INTERVAL.ago)
.update_all(last_repository_updated_at: created_at)
end
diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb
index dbd82dda06e..f50f28deffe 100644
--- a/app/models/internal_id.rb
+++ b/app/models/internal_id.rb
@@ -24,12 +24,9 @@ class InternalId < ActiveRecord::Base
#
# The operation locks the record and gathers a `ROW SHARE` lock (in PostgreSQL).
# As such, the increment is atomic and safe to be called concurrently.
- #
- # If a `maximum_iid` is passed in, this overrides the incremented value if it's
- # greater than that. This can be used to correct the increment value if necessary.
- def increment_and_save!(maximum_iid)
+ def increment_and_save!
lock!
- self.last_value = [(last_value || 0) + 1, (maximum_iid || 0) + 1].max
+ self.last_value = (last_value || 0) + 1
save!
last_value
end
@@ -93,16 +90,7 @@ class InternalId < ActiveRecord::Base
# and increment its last value
#
# Note this will acquire a ROW SHARE lock on the InternalId record
-
- # Note we always calculate the maximum iid present here and
- # pass it in to correct the InternalId entry if it's last_value is off.
- #
- # This can happen in a transition phase where both `AtomicInternalId` and
- # `NonatomicInternalId` code runs (e.g. during a deploy).
- #
- # This is subject to be cleaned up with the 10.8 release:
- # https://gitlab.com/gitlab-org/gitlab-ce/issues/45389.
- (lookup || create_record).increment_and_save!(maximum_iid)
+ (lookup || create_record).increment_and_save!
end
end
@@ -128,15 +116,11 @@ class InternalId < ActiveRecord::Base
InternalId.create!(
**scope,
usage: usage_value,
- last_value: maximum_iid
+ last_value: init.call(subject) || 0
)
end
rescue ActiveRecord::RecordNotUnique
lookup
end
-
- def maximum_iid
- @maximum_iid ||= init.call(subject) || 0
- end
end
end
diff --git a/app/models/merge_request_diff_file.rb b/app/models/merge_request_diff_file.rb
index 1199ff5af22..cd8ba6b904d 100644
--- a/app/models/merge_request_diff_file.rb
+++ b/app/models/merge_request_diff_file.rb
@@ -1,5 +1,6 @@
class MergeRequestDiffFile < ActiveRecord::Base
include Gitlab::EncodingHelper
+ include DiffFile
belongs_to :merge_request_diff
@@ -12,10 +13,4 @@ class MergeRequestDiffFile < ActiveRecord::Base
def diff
binary? ? super.unpack('m0').first : super
end
-
- def to_hash
- keys = Gitlab::Git::Diff::SERIALIZE_KEYS - [:diff]
-
- as_json(only: keys).merge(diff: diff).with_indifferent_access
- end
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 109405d3f17..02f7a9b1e4f 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -63,6 +63,7 @@ class Note < ActiveRecord::Base
has_many :todos
has_many :events, as: :target, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_one :system_note_metadata
+ has_one :note_diff_file, inverse_of: :diff_note, foreign_key: :diff_note_id
delegate :gfm_reference, :local_reference, to: :noteable
delegate :name, to: :project, prefix: true
@@ -100,7 +101,8 @@ class Note < ActiveRecord::Base
scope :inc_author_project, -> { includes(:project, :author) }
scope :inc_author, -> { includes(:author) }
scope :inc_relations_for_view, -> do
- includes(:project, :author, :updated_by, :resolved_by, :award_emoji, :system_note_metadata)
+ includes(:project, :author, :updated_by, :resolved_by, :award_emoji,
+ :system_note_metadata, :note_diff_file)
end
scope :diff_notes, -> { where(type: %w(LegacyDiffNote DiffNote)) }
diff --git a/app/models/note_diff_file.rb b/app/models/note_diff_file.rb
new file mode 100644
index 00000000000..e688018a6d9
--- /dev/null
+++ b/app/models/note_diff_file.rb
@@ -0,0 +1,7 @@
+class NoteDiffFile < ActiveRecord::Base
+ include DiffFile
+
+ belongs_to :diff_note, inverse_of: :note_diff_file
+
+ validates :diff_note, presence: true
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index 0fe9f8880b4..e275ac4dc6f 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -24,6 +24,7 @@ class Project < ActiveRecord::Base
include ChronicDurationAttribute
include FastDestroyAll::Helpers
include WithUploads
+ include BatchDestroyDependentAssociations
extend Gitlab::ConfigHelper
diff --git a/app/services/application_settings/update_service.rb b/app/services/application_settings/update_service.rb
index d6d3a661dab..e70445cfb67 100644
--- a/app/services/application_settings/update_service.rb
+++ b/app/services/application_settings/update_service.rb
@@ -3,6 +3,10 @@ module ApplicationSettings
def execute
update_terms(@params.delete(:terms))
+ if params.key?(:performance_bar_allowed_group_path)
+ params[:performance_bar_allowed_group_id] = performance_bar_allowed_group_id
+ end
+
@application_setting.update(@params)
end
@@ -18,5 +22,13 @@ module ApplicationSettings
ApplicationSetting::Term.create(terms: terms)
@application_setting.reset_memoized_terms
end
+
+ def performance_bar_allowed_group_id
+ performance_bar_enabled = !params.key?(:performance_bar_enabled) || params.delete(:performance_bar_enabled)
+ group_full_path = params.delete(:performance_bar_allowed_group_path)
+ return nil unless Gitlab::Utils.to_boolean(performance_bar_enabled)
+
+ Group.find_by_full_path(group_full_path)&.id if group_full_path.present?
+ end
end
end
diff --git a/app/services/check_gcp_project_billing_service.rb b/app/services/check_gcp_project_billing_service.rb
deleted file mode 100644
index ea82b61b279..00000000000
--- a/app/services/check_gcp_project_billing_service.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-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
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 077d27c5836..de0125ed0dd 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -137,7 +137,13 @@ module Projects
trash_repositories!
- project.team.truncate
+ # Rails attempts to load all related records into memory before
+ # destroying: https://github.com/rails/rails/issues/22510
+ # This ensures we delete records in batches.
+ #
+ # Exclude container repositories because its before_destroy would be
+ # called multiple times, and it doesn't destroy any database records.
+ project.destroy_dependent_associations_in_batches(exclude: [:container_repositories])
project.destroy!
end
end
diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb
index f2a8afccdeb..3fd27d9acdc 100644
--- a/app/uploaders/object_storage.rb
+++ b/app/uploaders/object_storage.rb
@@ -11,7 +11,7 @@ module ObjectStorage
ObjectStorageUnavailable = Class.new(StandardError)
DIRECT_UPLOAD_TIMEOUT = 4.hours
- TMP_UPLOAD_PATH = 'tmp/upload'.freeze
+ TMP_UPLOAD_PATH = 'tmp/uploads'.freeze
module Store
LOCAL = 1
diff --git a/app/views/admin/application_settings/_performance_bar.html.haml b/app/views/admin/application_settings/_performance_bar.html.haml
index 8001b42e2f9..030e8610b47 100644
--- a/app/views/admin/application_settings/_performance_bar.html.haml
+++ b/app/views/admin/application_settings/_performance_bar.html.haml
@@ -9,8 +9,8 @@
= f.check_box :performance_bar_enabled
Enable the Performance Bar
.form-group.row
- = f.label :performance_bar_allowed_group_id, 'Allowed group', class: 'col-form-label col-sm-2'
+ = f.label :performance_bar_allowed_group_path, 'Allowed group', class: 'col-form-label col-sm-2'
.col-sm-10
- = f.text_field :performance_bar_allowed_group_id, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
+ = f.text_field :performance_bar_allowed_group_path, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_repository_mirrors_form.html.haml b/app/views/admin/application_settings/_repository_mirrors_form.html.haml
index 9d05a5aa234..edd8e5e9eb8 100644
--- a/app/views/admin/application_settings/_repository_mirrors_form.html.haml
+++ b/app/views/admin/application_settings/_repository_mirrors_form.html.haml
@@ -9,7 +9,7 @@
= f.label :mirror_available do
= f.check_box :mirror_available
Allow mirrors to be setup for projects
- %span.help-block
+ %span.form-text.text-muted
If disabled, only admins will be able to setup mirrors in projects.
= link_to icon('question-circle'), help_page_path('workflow/repository_mirroring')
diff --git a/app/views/admin/application_settings/_signin.html.haml b/app/views/admin/application_settings/_signin.html.haml
index 4d74568d69a..83a30504222 100644
--- a/app/views/admin/application_settings/_signin.html.haml
+++ b/app/views/admin/application_settings/_signin.html.haml
@@ -23,7 +23,7 @@
must be used to authenticate.
- if omniauth_enabled? && button_based_providers.any?
.form-group.row
- = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'control-label col-sm-2'
+ = f.label :enabled_oauth_sign_in_sources, 'Enabled OAuth sign-in sources', class: 'col-form-label col-sm-2'
= hidden_field_tag 'application_setting[enabled_oauth_sign_in_sources][]'
.col-sm-10
.btn-group{ data: { toggle: 'buttons' } }
diff --git a/app/views/admin/application_settings/_terms.html.haml b/app/views/admin/application_settings/_terms.html.haml
index 32b060972ec..44bf7b65a8e 100644
--- a/app/views/admin/application_settings/_terms.html.haml
+++ b/app/views/admin/application_settings/_terms.html.haml
@@ -8,7 +8,7 @@
= f.label :enforce_terms do
= f.check_box :enforce_terms
= _("Require all users to accept Terms of Service when they access GitLab.")
- .help-block
+ .form-text.text-muted
= _("When enabled, users cannot use GitLab until the terms have been accepted.")
.form-group
.col-sm-12
@@ -16,7 +16,7 @@
= _("Terms of Service Agreement")
.col-sm-12
= f.text_area :terms, class: 'form-control', rows: 8
- .help-block
+ .form-text.text-muted
= _("Markdown enabled")
= f.submit _("Save changes"), class: "btn btn-success"
diff --git a/app/views/admin/application_settings/_visibility_and_access.html.haml b/app/views/admin/application_settings/_visibility_and_access.html.haml
index c37a89237f0..0f2524047e3 100644
--- a/app/views/admin/application_settings/_visibility_and_access.html.haml
+++ b/app/views/admin/application_settings/_visibility_and_access.html.haml
@@ -27,7 +27,7 @@
.form-check
= level
%span.form-text.text-muted#restricted-visibility-help
- Selected levels cannot be used by non-admin users for projects or snippets.
+ Selected levels cannot be used by non-admin users for groups, projects or snippets.
If the public level is restricted, user profiles are only visible to logged in users.
.form-group.row
= f.label :import_sources, class: 'col-form-label col-sm-2'
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 3df4ce93fa8..3cdeb103bb8 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -12,7 +12,7 @@
= link_to admin_projects_path do
%h3.text-center
Projects:
- = approximate_count_with_delimiters(Project)
+ = approximate_count_with_delimiters(@counts, Project)
%hr
= link_to('New project', new_project_path, class: "btn btn-new")
.col-sm-4
@@ -21,7 +21,7 @@
= link_to admin_users_path do
%h3.text-center
Users:
- = approximate_count_with_delimiters(User)
+ = approximate_count_with_delimiters(@counts, User)
= render_if_exists 'users_statistics'
%hr
= link_to 'New user', new_admin_user_path, class: "btn btn-new"
@@ -31,7 +31,7 @@
= link_to admin_groups_path do
%h3.text-center
Groups:
- = approximate_count_with_delimiters(Group)
+ = approximate_count_with_delimiters(@counts, Group)
%hr
= link_to 'New group', new_admin_group_path, class: "btn btn-new"
.row
@@ -42,31 +42,31 @@
%p
Forks
%span.light.float-right
- = approximate_count_with_delimiters(ForkedProjectLink)
+ = approximate_count_with_delimiters(@counts, ForkedProjectLink)
%p
Issues
%span.light.float-right
- = approximate_count_with_delimiters(Issue)
+ = approximate_count_with_delimiters(@counts, Issue)
%p
Merge Requests
%span.light.float-right
- = approximate_count_with_delimiters(MergeRequest)
+ = approximate_count_with_delimiters(@counts, MergeRequest)
%p
Notes
%span.light.float-right
- = approximate_count_with_delimiters(Note)
+ = approximate_count_with_delimiters(@counts, Note)
%p
Snippets
%span.light.float-right
- = approximate_count_with_delimiters(Snippet)
+ = approximate_count_with_delimiters(@counts, Snippet)
%p
SSH Keys
%span.light.float-right
- = approximate_count_with_delimiters(Key)
+ = approximate_count_with_delimiters(@counts, Key)
%p
Milestones
%span.light.float-right
- = approximate_count_with_delimiters(Milestone)
+ = approximate_count_with_delimiters(@counts, Milestone)
%p
Active Users
%span.light.float-right
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 5ec612d0c72..6d75ccd5add 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -13,7 +13,7 @@
.card
.card-header
Group info:
- %ul.well-list
+ %ul.content-list
%li
.avatar-container.s60
= group_icon(@group, class: "avatar s60")
@@ -64,7 +64,7 @@
Projects
%span.badge.badge-pill
#{@group.projects.count}
- %ul.well-list
+ %ul.content-list
- @projects.each do |project|
%li
%strong
@@ -82,7 +82,7 @@
Projects shared with #{@group.name}
%span.badge.badge-pill
#{@group.shared_projects.count}
- %ul.well-list
+ %ul.content-list
- @group.shared_projects.sort_by(&:name).each do |project|
%li
%strong
@@ -118,7 +118,7 @@
%span.badge.badge-pill= @group.members.size
.float-right
= link_to icon('pencil-square-o', text: 'Manage access'), polymorphic_url([@group, :members]), class: "btn btn-sm"
- %ul.well-list.group-users-list.content-list.members-list
+ %ul.content-list.group-users-list.content-list.members-list
= render partial: 'shared/members/member', collection: @members, as: :member, locals: { show_controls: false }
.card-footer
= paginate @members, param_name: 'members_page', theme: 'gitlab'
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 29ec712b6b7..0a22a142858 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -22,7 +22,7 @@
.card
.card-header
Project info:
- %ul.well-list
+ %ul.content-list
%li
%span.light Name:
%strong
@@ -166,7 +166,7 @@
.float-right
= link_to admin_group_path(@group), class: 'btn btn-sm' do
= icon('pencil-square-o', text: 'Manage access')
- %ul.well-list.content-list.members-list
+ %ul.content-list.members-list
= render partial: 'shared/members/member', collection: @group_members, as: :member, locals: { show_controls: false }
.card-footer
= paginate @group_members, param_name: 'group_members_page', theme: 'gitlab'
@@ -180,7 +180,7 @@
%span.badge.badge-pill= @project.users.size
.float-right
= link_to icon('pencil-square-o', text: 'Manage access'), polymorphic_url([@project, :members]), class: "btn btn-sm"
- %ul.well-list.project_members.content-list.members-list
+ %ul.content-list.project_members.members-list
= render partial: 'shared/members/member', collection: @project_members, as: :member, locals: { show_controls: false }
.card-footer
= paginate @project_members, param_name: 'project_members_page', theme: 'gitlab'
diff --git a/app/views/admin/users/_profile.html.haml b/app/views/admin/users/_profile.html.haml
index af22652e07c..4fcb9aad343 100644
--- a/app/views/admin/users/_profile.html.haml
+++ b/app/views/admin/users/_profile.html.haml
@@ -1,7 +1,7 @@
.card
.card-header
Profile
- %ul.well-list
+ %ul.content-list
%li
%span.light Member since
%strong= user.created_at.to_s(:medium)
diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml
index 469a7bd9715..cf50d45f755 100644
--- a/app/views/admin/users/projects.html.haml
+++ b/app/views/admin/users/projects.html.haml
@@ -4,7 +4,7 @@
- if @user.groups.any?
.card
.card-header Group projects
- %ul.card-body-list
+ %ul.hover-list
- @user.group_members.includes(:source).each do |group_member|
- group = group_member.group
%li.group_member
@@ -28,7 +28,7 @@
.col-md-6
.card
.card-header Joined projects (#{@joined_projects.count})
- %ul.card-body-list
+ %ul.hover-list
- @joined_projects.sort_by(&:full_name).each do |project|
- member = project.team.find_member(@user.id)
%li.project_member
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index a74fcea65d8..b0562226f5f 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -8,7 +8,7 @@
.card
.card-header
= @user.name
- %ul.well-list
+ %ul.content-list
%li
= image_tag avatar_icon_for_user(@user, 60), class: "avatar s60"
%li
@@ -21,7 +21,7 @@
.card
.card-header
Account:
- %ul.well-list
+ %ul.content-list
%li
%span.light Name:
%strong= @user.name
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index f85f5c5be88..85f2d00bde3 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -14,7 +14,7 @@
- if event.push_with_commits?
.event-body
- %ul.well-list.event_commits
+ %ul.content-list.event_commits
= render "events/commit", project: project, event: event
- create_mr = event.new_ref? && create_mr_button?(project.default_branch, event.ref_name, project) && event.authored_by?(current_user)
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 96ed63937fa..cae2df4699e 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -1,78 +1,39 @@
- breadcrumb_title "General Settings"
- @content_class = "limit-container-width" unless fluid_layout
-
-.card.prepend-top-default
- .card-header
- Group settings
- .card-body
- = form_for @group, html: { multipart: true, class: "gl-show-field-errors" }, authenticity_token: true do |f|
- = form_errors(@group)
- = render 'shared/group_form', f: f
-
- .form-group.row
- .offset-sm-2.col-sm-10
- .avatar-container.s160
- = group_icon(@group, alt: '', class: 'avatar group-avatar s160')
- %p.light
- - if @group.avatar?
- You can change the group avatar here
- - else
- You can upload a group avatar here
- = render 'shared/choose_group_avatar_button', f: f
- - if @group.avatar?
- %hr
- = link_to _('Remove avatar'), group_avatar_path(@group.to_param), data: { confirm: _("Avatar will be removed. Are you sure?")}, method: :delete, class: "btn btn-danger btn-inverted"
-
- = render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
-
- .form-group.row
- .offset-sm-2.col-sm-10
- = render 'shared/allow_request_access', form: f
-
- .form-group.row
- %label.col-form-label.col-sm-2
- = s_("GroupSettings|Share with group lock")
- .col-sm-10
- .form-check
- = f.label :share_with_group_lock do
- = f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group)
- %strong
- - group_link = link_to @group.name, group_path(@group)
- = s_("GroupSettings|Prevent sharing a project within %{group} with other groups").html_safe % { group: group_link }
- %br
- %span.descr= share_with_group_lock_help_text(@group)
-
- = render 'group_admin_settings', f: f
-
- .form-actions
- = f.submit 'Save group', class: "btn btn-save"
-
-.card.bg-danger
- .card-header Remove group
- .card-body
- = form_tag(@group, method: :delete) do
- %p
- Removing group will cause all child projects and resources to be removed.
- %br
- %strong Removed group can not be restored!
-
- .form-actions
- = button_to 'Remove group', '#', class: "btn btn-remove js-confirm-danger", data: { "confirm-danger-message" => remove_group_message(@group) }
-
-- if supports_nested_groups?
- .card.bg-warning
- .card-header Transfer group
- .card-body
- = form_for @group, url: transfer_group_path(@group), method: :put do |f|
- .form-group
- = dropdown_tag('Select parent group', options: { toggle_class: 'js-groups-dropdown', title: 'Parent Group', filter: true, dropdown_class: 'dropdown-open-top dropdown-group-transfer', placeholder: "Search groups", data: { data: parent_group_options(@group) } })
- = hidden_field_tag 'new_parent_group_id'
-
- %ul
- %li Be careful. Changing a group's parent can have unintended #{link_to 'side effects', 'https://docs.gitlab.com/ce/user/project/index.html#redirects-when-changing-repository-paths', target: 'blank'}.
- %li You can only transfer the group to a group you manage.
- %li You will need to update your local repositories to point to the new location.
- %li If the parent group's visibility is lower than the group current visibility, visibility levels for subgroups and projects will be changed to match the new parent group's visibility.
- = f.submit 'Transfer group', class: "btn btn-warning"
+- expanded = Rails.env.test?
+
+
+%section.settings.gs-general.no-animate#js-general-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('General')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = _('Update your group name, description, avatar, and other general settings.')
+ .settings-content
+ = render 'groups/settings/general'
+
+%section.settings.gs-permissions.no-animate#js-permissions-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Permissions')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = _('Enable or disable certain group features and choose access levels.')
+ .settings-content
+ = render 'groups/settings/permissions'
+
+%section.settings.gs-advanced.no-animate#js-advanced-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4
+ = _('Advanced')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
+ %p
+ = _('Perform advanced options such as changing path, transferring, or removing the group.')
+ .settings-content
+ = render 'groups/settings/advanced'
= render 'shared/confirm_modal', phrase: @group.path
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index cd07b95155c..ba186875a86 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -8,7 +8,7 @@
.controls
= link_to new_project_path(namespace_id: @group.id), class: "btn btn-sm btn-success" do
New project
- %ul.well-list
+ %ul.content-list
- @projects.each do |project|
%li
.list-item-name
diff --git a/app/views/groups/runners/_runner.html.haml b/app/views/groups/runners/_runner.html.haml
index 76650a961d6..3f89b04a5fc 100644
--- a/app/views/groups/runners/_runner.html.haml
+++ b/app/views/groups/runners/_runner.html.haml
@@ -8,13 +8,13 @@
= link_to edit_group_runner_path(@group, runner) do
= icon('edit')
- .pull-right
+ .float-right
- if runner.active?
= link_to _('Pause'), pause_group_runner_path(@group, runner), method: :post, class: 'btn btn-sm btn-danger', data: { confirm: _("Are you sure?") }
- else
= link_to _('Resume'), resume_group_runner_path(@group, runner), method: :post, class: 'btn btn-success btn-sm'
= link_to _('Remove Runner'), group_runner_path(@group, runner), data: { confirm: _("Are you sure?") }, method: :delete, class: 'btn btn-danger btn-sm'
- .pull-right
+ .float-right
%small.light
\##{runner.id}
- if runner.description.present?
diff --git a/app/views/groups/settings/_advanced.html.haml b/app/views/groups/settings/_advanced.html.haml
new file mode 100644
index 00000000000..b7c673db705
--- /dev/null
+++ b/app/views/groups/settings/_advanced.html.haml
@@ -0,0 +1,49 @@
+.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'
diff --git a/app/views/groups/settings/_general.html.haml b/app/views/groups/settings/_general.html.haml
new file mode 100644
index 00000000000..64786d24266
--- /dev/null
+++ b/app/views/groups/settings/_general.html.haml
@@ -0,0 +1,38 @@
+= 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'
diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml
new file mode 100644
index 00000000000..15a5ecf791c
--- /dev/null
+++ b/app/views/groups/settings/_permissions.html.haml
@@ -0,0 +1,28 @@
+= 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'
diff --git a/app/views/groups/settings/ci_cd/show.html.haml b/app/views/groups/settings/ci_cd/show.html.haml
index 082e1b7befa..383d955d71f 100644
--- a/app/views/groups/settings/ci_cd/show.html.haml
+++ b/app/views/groups/settings/ci_cd/show.html.haml
@@ -6,7 +6,7 @@
%section.settings#secret-variables.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
- = _('Secret variables')
+ = _('Variables')
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer'
%button.btn.btn-default.js-settings-toggle{ type: "button" }
= expanded ? _('Collapse') : _('Expand')
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index 6391a13dd25..7a66bac09cb 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -36,7 +36,7 @@
.card
.card-header
Quick help
- %ul.well-list
+ %ul.content-list
%li= link_to 'See our website for getting help', support_url
%li
%button.btn-blank.btn-link.js-trigger-search-bar{ type: 'button' }
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index 94b012d39a3..a06db85ef6f 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -116,9 +116,9 @@
.lead
List with hover effect
- %code .well-list
+ %code .hover-list
.example
- %ul.well-list
+ %ul.hover-list
%li
One item
%li
@@ -131,7 +131,7 @@
.example
.card
.card-header Your list
- %ul.well-list
+ %ul.content-list
%li
One item
%li
diff --git a/app/views/profiles/_event_table.html.haml b/app/views/profiles/_event_table.html.haml
index 37466f7c821..9f525547dd9 100644
--- a/app/views/profiles/_event_table.html.haml
+++ b/app/views/profiles/_event_table.html.haml
@@ -1,7 +1,7 @@
%h5.prepend-top-0
History of authentications
-%ul.well-list
+%ul.content-list
- events.each do |event|
%li
%span.description
diff --git a/app/views/profiles/active_sessions/_active_session.html.haml b/app/views/profiles/active_sessions/_active_session.html.haml
index d198bfc80db..23ef31a0c85 100644
--- a/app/views/profiles/active_sessions/_active_session.html.haml
+++ b/app/views/profiles/active_sessions/_active_session.html.haml
@@ -1,10 +1,10 @@
- is_current_session = active_session.current?(session)
%li.list-group-item
- .pull-left.append-right-10{ data: { toggle: 'tooltip' }, title: active_session.human_device_type }
+ .float-left.append-right-10{ data: { toggle: 'tooltip' }, title: active_session.human_device_type }
= active_session_device_type_icon(active_session)
- .description.pull-left
+ .description.float-left
%div
%strong= active_session.ip_address
- if is_current_session
@@ -25,7 +25,7 @@
= l(active_session.created_at, format: :short)
- unless is_current_session
- .pull-right
+ .float-right
= link_to profile_active_session_path(active_session.session_id), data: { confirm: 'Are you sure? The device will be signed out of GitLab.' }, method: :delete, class: "btn btn-danger prepend-left-10" do
%span.sr-only Revoke
Revoke
diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml
index 914bd4eb57c..a5db9dbe7f8 100644
--- a/app/views/profiles/emails/index.html.haml
+++ b/app/views/profiles/emails/index.html.haml
@@ -29,7 +29,7 @@
Your Public Email will be displayed on your public profile.
%li
All email addresses will be used to identify your commits.
- %ul.well-list
+ %ul.content-list
%li
= render partial: 'shared/email_with_badge', locals: { email: @primary_email, verified: current_user.confirmed? }
%span.float-right
diff --git a/app/views/profiles/gpg_keys/_key_table.html.haml b/app/views/profiles/gpg_keys/_key_table.html.haml
index cabb92c5a24..b9b60c218fd 100644
--- a/app/views/profiles/gpg_keys/_key_table.html.haml
+++ b/app/views/profiles/gpg_keys/_key_table.html.haml
@@ -1,7 +1,7 @@
- is_admin = local_assigns.fetch(:admin, false)
- if @gpg_keys.any?
- %ul.well-list
+ %ul.content-list
= render partial: 'profiles/gpg_keys/key', collection: @gpg_keys, locals: { is_admin: is_admin }
- else
%p.settings-message.text-center
diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml
index 02d5b08f7a3..2ac514d3f6f 100644
--- a/app/views/profiles/keys/_key_details.html.haml
+++ b/app/views/profiles/keys/_key_details.html.haml
@@ -4,7 +4,7 @@
.card
.card-header
SSH Key
- %ul.well-list
+ %ul.content-list
%li
%span.light Title:
%strong= @key.title
diff --git a/app/views/profiles/keys/_key_table.html.haml b/app/views/profiles/keys/_key_table.html.haml
index e78763bdcb2..e088140fdd2 100644
--- a/app/views/profiles/keys/_key_table.html.haml
+++ b/app/views/profiles/keys/_key_table.html.haml
@@ -1,7 +1,7 @@
- is_admin = local_assigns.fetch(:admin, false)
- if @keys.any?
- %ul.well-list
+ %ul.content-list
= render partial: 'profiles/keys/key', collection: @keys, locals: { is_admin: is_admin }
- else
%p.settings-message.text-center
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 5b1f2d8953b..f641d7bc51a 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -28,7 +28,7 @@
= s_('Branches|Cant find HEAD commit for this branch')
- if branch.name != @repository.root_ref
- .divergence-graph.d-none.d-sm-block{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind),
+ .divergence-graph.d-none.d-md-block{ title: s_('%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead') % { number_commits_behind: diverging_count_label(number_commits_behind),
default_branch: @repository.root_ref,
number_commits_ahead: diverging_count_label(number_commits_ahead) } }
.graph-side
@@ -39,7 +39,7 @@
.bar.bar-ahead{ style: "width: #{number_commits_ahead * bar_graph_width_factor}%" }
%span.count.count-ahead= diverging_count_label(number_commits_ahead)
- .controls.d-none.d-sm-block<
+ .controls.d-none.d-md-block<
- if merge_project && create_mr_button?(@repository.root_ref, branch.name)
= link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-default' do
= _('Merge request')
diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml
index 5739a57dcfe..ca7a6d5a886 100644
--- a/app/views/projects/clusters/gcp/_form.html.haml
+++ b/app/views/projects/clusters/gcp/_form.html.haml
@@ -1,8 +1,10 @@
+= javascript_include_tag 'https://apis.google.com/js/api.js'
+
%p
- link_to_help_page = link_to(s_('ClusterIntegration|help page'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
= s_('ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration.').html_safe % { link_to_help_page: link_to_help_page}
-= form_for @cluster, html: { class: 'prepend-top-20' }, url: gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
+= form_for @cluster, html: { class: 'js-gke-cluster-creation prepend-top-20', data: { token: token_in_session } }, url: gcp_namespace_project_clusters_path(@project.namespace, @project), as: :cluster do |field|
= form_errors(@cluster)
.form-group
= field.label :name, s_('ClusterIntegration|Kubernetes cluster name')
@@ -14,13 +16,25 @@
= field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field|
.form-group
= provider_gcp_field.label :gcp_project_id, s_('ClusterIntegration|Google Cloud Platform project ID')
- = link_to(s_('ClusterIntegration|See your projects'), 'https://console.cloud.google.com/home/dashboard', target: '_blank', rel: 'noopener noreferrer')
- = provider_gcp_field.text_field :gcp_project_id, class: 'form-control', placeholder: s_('ClusterIntegration|Project ID')
+ .js-gcp-project-id-dropdown-entry-point{ data: { docsUrl: 'https://console.cloud.google.com/home/dashboard' } }
+ = provider_gcp_field.hidden_field :gcp_project_id
+ .dropdown
+ %button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', disabled: true }
+ %span.dropdown-toggle-text
+ = _('Select project')
+ = icon('chevron-down')
+ %span.form-text.text-muted &nbsp;
.form-group
= provider_gcp_field.label :zone, s_('ClusterIntegration|Zone')
= link_to(s_('ClusterIntegration|See zones'), 'https://cloud.google.com/compute/docs/regions-zones/regions-zones', target: '_blank', rel: 'noopener noreferrer')
- = provider_gcp_field.text_field :zone, class: 'form-control', placeholder: 'us-central1-a'
+ .js-gcp-zone-dropdown-entry-point
+ = provider_gcp_field.hidden_field :zone
+ .dropdown
+ %button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', disabled: true }
+ %span.dropdown-toggle-text
+ = _('Select project to choose zone')
+ = icon('chevron-down')
.form-group
= provider_gcp_field.label :num_nodes, s_('ClusterIntegration|Number of nodes')
@@ -28,8 +42,13 @@
.form-group
= provider_gcp_field.label :machine_type, s_('ClusterIntegration|Machine type')
- = link_to(s_('ClusterIntegration|See machine types'), 'https://cloud.google.com/compute/docs/machine-types', target: '_blank', rel: 'noopener noreferrer')
- = provider_gcp_field.text_field :machine_type, class: 'form-control', placeholder: 'n1-standard-4'
+ .js-gcp-machine-type-dropdown-entry-point
+ = provider_gcp_field.hidden_field :machine_type
+ .dropdown
+ %button.dropdown-menu-toggle.dropdown-menu-full-width{ type: 'button', disabled: true }
+ %span.dropdown-toggle-text
+ = _('Select project and zone to choose machine type')
+ = icon('chevron-down')
.form-group
- = field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'btn btn-success'
+ = field.submit s_('ClusterIntegration|Create Kubernetes cluster'), class: 'js-gke-cluster-creation-submit btn btn-success', disabled: true
diff --git a/app/views/projects/hooks/_index.html.haml b/app/views/projects/hooks/_index.html.haml
index 776681ea09a..5990582fd55 100644
--- a/app/views/projects/hooks/_index.html.haml
+++ b/app/views/projects/hooks/_index.html.haml
@@ -15,7 +15,7 @@
%h5.prepend-top-default
Webhooks (#{@hooks.count})
- if @hooks.any?
- %ul.well-list
+ %ul.content-list
- @hooks.each do |hook|
= render 'project_hook', hook: hook
- else
diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml
index 9c9e4ef8fce..459150c1067 100644
--- a/app/views/projects/jobs/_sidebar.html.haml
+++ b/app/views/projects/jobs/_sidebar.html.haml
@@ -18,7 +18,7 @@
%span= time_ago_with_tooltip @build.artifacts_expire_at
- if @build.artifacts?
- .btn-group.btn-group.d-flex{ role: :group }
+ .btn-group.d-flex{ role: :group }
- if @build.has_expiring_artifacts? && can?(current_user, :update_build, @build)
= link_to keep_project_job_artifacts_path(@project, @build), class: 'btn btn-sm btn-default', method: :post do
Keep
@@ -42,7 +42,7 @@
- if @build.trigger_variables.any?
%p
- %button.btn.group.btn-group.js-reveal-variables Reveal Variables
+ %button.btn.group.js-reveal-variables Reveal Variables
%dl.js-build-variables.trigger-build-variables.hide
- @build.trigger_variables.each do |trigger_variable|
diff --git a/app/views/projects/mirrors/_push.html.haml b/app/views/projects/mirrors/_push.html.haml
index 4a6aefce351..c3dcd9617a6 100644
--- a/app/views/projects/mirrors/_push.html.haml
+++ b/app/views/projects/mirrors/_push.html.haml
@@ -30,7 +30,7 @@
#{h(@remote_mirror.last_error.strip)}
= f.fields_for :remote_mirrors, @remote_mirror do |rm_form|
.form-group
- = rm_form.check_box :enabled, class: "pull-left"
+ = rm_form.check_box :enabled, class: "float-left"
.prepend-left-20
= rm_form.label :enabled, "Remote mirror repository", class: "label-light append-bottom-0"
%p.light.append-bottom-0
@@ -42,7 +42,7 @@
= render "projects/mirrors/instructions"
.form-group
- = rm_form.check_box :only_protected_branches, class: 'pull-left'
+ = rm_form.check_box :only_protected_branches, class: 'float-left'
.prepend-left-20
= rm_form.label :only_protected_branches, class: 'label-light'
= link_to icon('question-circle'), help_page_path('user/project/protected_branches')
diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml
index 986ca852411..e7178f9160c 100644
--- a/app/views/projects/pages/_list.html.haml
+++ b/app/views/projects/pages/_list.html.haml
@@ -4,7 +4,7 @@
.card
.card-header
Domains (#{@domains.count})
- %ul.well-list.pages-domain-list{ class: ("has-verification-status" if verification_enabled) }
+ %ul.content-list.pages-domain-list{ class: ("has-verification-status" if verification_enabled) }
- @domains.each do |domain|
%li.pages-domain-list-item.unstyled
- if verification_enabled
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index d1e8e9d0d60..956f8fef6b8 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -29,7 +29,7 @@
.form-actions
= f.submit s_('Pipeline|Create pipeline'), class: 'btn btn-success js-variables-save-button', tabindex: 3
- = link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-default pull-right'
+ = link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-default float-right'
-# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
index 988bcfb5265..414df15feeb 100644
--- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
@@ -34,7 +34,7 @@
= form.label :domain, class:"prepend-top-10" do
= _('Domain')
= form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
- .help-block
+ .form-text.text-muted
= s_('CICD|A domain is required to use Auto Review Apps and Auto Deploy Stages.')
- if cluster_ingress_ip = cluster_ingress_ip(@project)
= s_('%{nip_domain} can be used as an alternative to a custom domain.').html_safe % { nip_domain: "<code>#{cluster_ingress_ip}.nip.io</code>".html_safe }
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index 7d8dd58e7e0..ed17bd4f7dc 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -42,7 +42,7 @@
%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4
- = _('Secret variables')
+ = _('Variables')
= link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank', rel: 'noopener noreferrer'
%button.btn.js-settings-toggle{ type: 'button' }
= expanded ? 'Collapse' : 'Expand'
diff --git a/app/views/projects/wikis/empty.html.haml b/app/views/projects/wikis/empty.html.haml
index d6e568bac94..62fa6e1907b 100644
--- a/app/views/projects/wikis/empty.html.haml
+++ b/app/views/projects/wikis/empty.html.haml
@@ -1,6 +1,4 @@
- page_title _("Wiki")
+- @right_sidebar = false
-%h3.page-title= s_("Wiki|Empty page")
-%hr
-.error_message
- = s_("WikiEmptyPageError|You are not allowed to create wiki pages")
+= render 'shared/empty_states/wikis'
diff --git a/app/views/shared/_email_with_badge.html.haml b/app/views/shared/_email_with_badge.html.haml
index b7bbc109238..ad863b1967d 100644
--- a/app/views/shared/_email_with_badge.html.haml
+++ b/app/views/shared/_email_with_badge.html.haml
@@ -1,4 +1,4 @@
-- css_classes = %w(label label-verification-status)
+- css_classes = %w(badge badge-verification-status)
- css_classes << (verified ? 'verified': 'unverified')
- text = verified ? 'Verified' : 'Unverified'
diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml
index d67409ffe14..38c6f560dc6 100644
--- a/app/views/shared/_visibility_level.html.haml
+++ b/app/views/shared/_visibility_level.html.haml
@@ -1,6 +1,6 @@
- with_label = local_assigns.fetch(:with_label, true)
-.form-group.visibility-level-setting
+.form-group.row.visibility-level-setting
- if with_label
= f.label :visibility_level, class: 'col-form-label col-sm-2' do
Visibility Level
diff --git a/app/views/shared/empty_states/_wikis.html.haml b/app/views/shared/empty_states/_wikis.html.haml
new file mode 100644
index 00000000000..fabb1f39a34
--- /dev/null
+++ b/app/views/shared/empty_states/_wikis.html.haml
@@ -0,0 +1,30 @@
+- layout_path = 'shared/empty_states/wikis_layout'
+
+- if can?(current_user, :create_wiki, @project)
+ - create_path = project_wiki_path(@project, params[:id], { view: 'create' })
+ - create_link = link_to s_('WikiEmpty|Create your first page'), create_path, class: 'btn btn-new', title: s_('WikiEmpty|Create your first page')
+
+ = render layout: layout_path, locals: { image_path: 'illustrations/wiki_login_empty.svg' } do
+ %h4
+ = s_('WikiEmpty|The wiki lets you write documentation for your project')
+ %p.text-left
+ = s_("WikiEmpty|A wiki is where you can store all the details about your project. This can include why you've created it, it's principles, how to use it, and so on.")
+ = create_link
+
+- elsif can?(current_user, :read_issue, @project)
+ - issues_link = link_to s_('WikiEmptyIssueMessage|issue tracker'), project_issues_path(@project)
+ - new_issue_link = link_to s_('WikiEmpty|Suggest wiki improvement'), new_project_issue_path(@project), class: 'btn btn-new', title: s_('WikiEmptyIssueMessage|Suggest wiki improvement')
+
+ = render layout: layout_path, locals: { image_path: 'illustrations/wiki_logout_empty.svg' } do
+ %h4
+ = s_('WikiEmpty|This project has no wiki pages')
+ %p.text-left
+ = s_('WikiEmptyIssueMessage|You must be a project member in order to add wiki pages. If you have suggestions for how to improve the wiki for this project, consider opening an issue in the %{issues_link}.').html_safe % { issues_link: issues_link }
+ = new_issue_link
+
+- else
+ = render layout: layout_path, locals: { image_path: 'illustrations/wiki_logout_empty.svg' } do
+ %h4
+ = s_('WikiEmpty|This project has no wiki pages')
+ %p
+ = s_('WikiEmpty|You must be a project member in order to add wiki pages.')
diff --git a/app/views/shared/empty_states/_wikis_layout.html.haml b/app/views/shared/empty_states/_wikis_layout.html.haml
new file mode 100644
index 00000000000..6fae6104ca2
--- /dev/null
+++ b/app/views/shared/empty_states/_wikis_layout.html.haml
@@ -0,0 +1,7 @@
+.row.empty-state
+ .col-xs-12
+ .svg-content
+ = image_tag image_path
+ .col-xs-12
+ .text-content.text-center
+ = yield
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 602729b172a..a57cd4b20d1 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -33,7 +33,7 @@
= link_to _('Edit'), '#', class: 'js-sidebar-dropdown-toggle edit-link float-right'
.value.hide-collapsed
- if issuable.milestone
- = link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_tooltip_due_date(issuable.milestone), data: { container: "body", html: 'true' }
+ = link_to issuable.milestone.title, milestone_path(issuable.milestone), class: "bold has-tooltip", title: milestone_tooltip_due_date(issuable.milestone), data: { container: "body", html: 'true', boundary: 'viewport' }
- else
%span.no-value
= _('None')
diff --git a/app/views/shared/milestones/_issuables.html.haml b/app/views/shared/milestones/_issuables.html.haml
index d8e4d2ff88c..ee6354b1c28 100644
--- a/app/views/shared/milestones/_issuables.html.haml
+++ b/app/views/shared/milestones/_issuables.html.haml
@@ -11,7 +11,7 @@
= number_with_delimiter(issuables.length)
- class_prefix = dom_class(issuables).pluralize
- %ul{ class: "well-list milestone-#{class_prefix}-list", id: "#{class_prefix}-list-#{id}" }
+ %ul{ class: "content-list milestone-#{class_prefix}-list", id: "#{class_prefix}-list-#{id}" }
= render partial: 'shared/milestones/issuable',
collection: issuables,
as: :issuable,
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index 95138af3950..becd1c4884e 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -78,7 +78,7 @@
%span= milestone.issues_visible_to_user(current_user).count
.title.hide-collapsed
Issues
- %span.badg.badge-pille= milestone.issues_visible_to_user(current_user).count
+ %span.badge.badge-pill= milestone.issues_visible_to_user(current_user).count
- if show_new_issue_link?(project)
= link_to new_project_issue_path(project, issue: { milestone_id: milestone.id }), class: "float-right", title: "New Issue" do
New issue
@@ -99,6 +99,8 @@
= _('Time tracking')
= icon('spinner spin')
+ = render_if_exists 'shared/milestones/weight', milestone: milestone
+
.block.merge-requests
.sidebar-collapsed-icon.has-tooltip{ title: milestone_merge_requests_tooltip_text(milestone), data: { container: 'body', html: 'true', placement: 'left', boundary: 'viewport' } }
%strong
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index ee0e35cedc6..320e3788a0f 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -48,6 +48,8 @@
- close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.'
%span All issues for this milestone are closed. #{close_msg}
+= render_if_exists 'shared/milestones/burndown', milestone: @milestone, project: @project
+
- if is_dynamic_milestone
.table-holder
%table.table
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index e036b21b23f..5069e2e4ca6 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -28,7 +28,7 @@
= link_to user_snippets_path(snippet.author) do
= snippet.author_name
- if link_project && snippet.project_id?
- %span.d-none.d-sm-block
+ %span.d-none.d-sm-inline-block
in
= link_to project_path(snippet.project) do
= snippet.project.full_name
diff --git a/app/views/sherlock/queries/_backtrace.html.haml b/app/views/sherlock/queries/_backtrace.html.haml
index 4f5146cefb9..38b4d2c6102 100644
--- a/app/views/sherlock/queries/_backtrace.html.haml
+++ b/app/views/sherlock/queries/_backtrace.html.haml
@@ -3,7 +3,7 @@
.card-header
%strong
= t('sherlock.application_backtrace')
- %ul.well-list
+ %ul.content-list
- @query.application_backtrace.each do |location|
%li
%strong
@@ -19,7 +19,7 @@
.card-header
%strong
= t('sherlock.full_backtrace')
- %ul.well-list
+ %ul.content-list
- @query.backtrace.each do |location|
%li
- if location.application?
diff --git a/app/views/sherlock/queries/_general.html.haml b/app/views/sherlock/queries/_general.html.haml
index 34c0cc4da39..37747faed62 100644
--- a/app/views/sherlock/queries/_general.html.haml
+++ b/app/views/sherlock/queries/_general.html.haml
@@ -3,7 +3,7 @@
.card-header
%strong
= t('sherlock.general')
- %ul.well-list
+ %ul.content-list
%li
%span.light
#{t('sherlock.time')}:
@@ -32,7 +32,7 @@
= @query.formatted_query
%strong
= t('sherlock.query')
- %ul.well-list
+ %ul.content-list
%li
.code.js-syntax-highlight.sherlock-code
:preserve
@@ -47,7 +47,7 @@
= @query.explain
%strong
= t('sherlock.query_plan')
- %ul.well-list
+ %ul.content-list
%li
.code.js-syntax-highlight.sherlock-code
%pre
diff --git a/app/views/sherlock/transactions/_general.html.haml b/app/views/sherlock/transactions/_general.html.haml
index 7ec8dde8421..9c028b5c741 100644
--- a/app/views/sherlock/transactions/_general.html.haml
+++ b/app/views/sherlock/transactions/_general.html.haml
@@ -3,7 +3,7 @@
.card-header
%strong
= t('sherlock.general')
- %ul.well-list
+ %ul.content-list
%li
%span.light
#{t('sherlock.id')}:
diff --git a/app/views/users/terms/index.html.haml b/app/views/users/terms/index.html.haml
index c5406696bdd..e0fe551cf36 100644
--- a/app/views/users/terms/index.html.haml
+++ b/app/views/users/terms/index.html.haml
@@ -4,10 +4,10 @@
= markdown_field(@term, :terms)
.row-content-block.footer-block.clearfix
- if can?(current_user, :accept_terms, @term)
- .pull-right
+ .float-right
= button_to accept_term_path(@term, redirect_params), class: 'btn btn-success prepend-left-8' do
= _('Accept terms')
- if can?(current_user, :decline_terms, @term)
- .pull-right
+ .float-right
= button_to decline_term_path(@term, redirect_params), class: 'btn btn-default prepend-left-8' do
= _('Decline and sign out')
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index b6433eb3eff..93e57512edb 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -24,7 +24,6 @@
- gcp_cluster:cluster_provision
- gcp_cluster:cluster_wait_for_app_installation
- gcp_cluster:wait_for_cluster_creation
-- gcp_cluster:check_gcp_project_billing
- gcp_cluster:cluster_wait_for_ingress_ip_address
- github_import_advance_stage
@@ -115,3 +114,4 @@
- upload_checksum
- web_hook
- repository_update_remote_mirror
+- create_note_diff_file
diff --git a/app/workers/check_gcp_project_billing_worker.rb b/app/workers/check_gcp_project_billing_worker.rb
deleted file mode 100644
index 363f81590ab..00000000000
--- a/app/workers/check_gcp_project_billing_worker.rb
+++ /dev/null
@@ -1,92 +0,0 @@
-require 'securerandom'
-
-class CheckGcpProjectBillingWorker
- include ApplicationWorker
- include ClusterQueue
-
- LEASE_TIMEOUT = 3.seconds.to_i
- SESSION_KEY_TIMEOUT = 5.minutes
- BILLING_TIMEOUT = 1.hour
- BILLING_CHANGED_LABELS = { state_transition: nil }.freeze
-
- def self.get_session_token(token_key)
- Gitlab::Redis::SharedState.with do |redis|
- redis.get(get_redis_session_key(token_key))
- end
- end
-
- def self.store_session_token(token)
- generate_token_key.tap do |token_key|
- Gitlab::Redis::SharedState.with do |redis|
- redis.set(get_redis_session_key(token_key), token, ex: SESSION_KEY_TIMEOUT)
- end
- end
- end
-
- def self.get_billing_state(token)
- Gitlab::Redis::SharedState.with do |redis|
- value = redis.get(redis_shared_state_key_for(token))
- ActiveRecord::Type::Boolean.new.type_cast_from_user(value)
- end
- end
-
- def perform(token_key)
- return unless token_key
-
- token = self.class.get_session_token(token_key)
- return unless token
- return unless try_obtain_lease_for(token)
-
- billing_enabled_state = !CheckGcpProjectBillingService.new.execute(token).empty?
- update_billing_change_counter(self.class.get_billing_state(token), billing_enabled_state)
- self.class.set_billing_state(token, billing_enabled_state)
- end
-
- private
-
- def self.generate_token_key
- SecureRandom.uuid
- end
-
- def self.get_redis_session_key(token_key)
- "gitlab:gcp:session:#{token_key}"
- end
-
- def self.redis_shared_state_key_for(token)
- "gitlab:gcp:#{Digest::SHA1.hexdigest(token)}:billing_enabled"
- end
-
- def self.set_billing_state(token, value)
- Gitlab::Redis::SharedState.with do |redis|
- redis.set(redis_shared_state_key_for(token), value, ex: BILLING_TIMEOUT)
- end
- end
-
- def try_obtain_lease_for(token)
- Gitlab::ExclusiveLease
- .new("check_gcp_project_billing_worker:#{token.hash}", timeout: LEASE_TIMEOUT)
- .try_obtain
- end
-
- def billing_changed_counter
- @billing_changed_counter ||= Gitlab::Metrics.counter(
- :gcp_billing_change_count,
- "Counts the number of times a GCP project changed billing_enabled state from false to true",
- BILLING_CHANGED_LABELS
- )
- end
-
- def state_transition(previous_state, current_state)
- if previous_state.nil? && !current_state
- 'no_billing'
- elsif previous_state.nil? && current_state
- 'with_billing'
- elsif !previous_state && current_state
- 'billing_configured'
- end
- end
-
- def update_billing_change_counter(previous_state, current_state)
- billing_changed_counter.increment(state_transition: state_transition(previous_state, current_state))
- end
-end
diff --git a/app/workers/create_note_diff_file_worker.rb b/app/workers/create_note_diff_file_worker.rb
new file mode 100644
index 00000000000..624b638a24e
--- /dev/null
+++ b/app/workers/create_note_diff_file_worker.rb
@@ -0,0 +1,9 @@
+class CreateNoteDiffFileWorker
+ include ApplicationWorker
+
+ def perform(diff_note_id)
+ diff_note = DiffNote.find(diff_note_id)
+
+ diff_note.create_diff_file
+ end
+end