summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClement Ho <clemmakesapps@gmail.com>2018-05-29 14:21:28 +0000
committerClement Ho <clemmakesapps@gmail.com>2018-05-29 14:21:28 +0000
commitc1717225c2157d00658333376f1362d4084999cb (patch)
treead65fe748d57a7344b13a9e90816b33529e24556
parent88f28e0ec8259082a16636bbf2247e14eee927a0 (diff)
parent4c74936f4567ba142bcd9ca31c8f5f10c8aa52fa (diff)
downloadgitlab-ce-bootstrap-fixes-from-ee.tar.gz
Merge branch 'master' into 'bootstrap-fixes-from-ee'bootstrap-fixes-from-ee
# Conflicts: # app/assets/javascripts/jobs/components/sidebar_details_block.vue
-rw-r--r--CONTRIBUTING.md2
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile4
-rw-r--r--Gemfile.lock12
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_details_block.vue10
-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/wikis/wikis.js9
-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/framework/dropdowns.scss4
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss102
-rw-r--r--app/controllers/admin/dashboard_controller.rb4
-rw-r--r--app/controllers/projects/clusters/gcp_controller.rb32
-rw-r--r--app/controllers/projects/wikis_controller.rb8
-rw-r--r--app/finders/group_projects_finder.rb26
-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/concerns/diff_file.rb9
-rw-r--r--app/models/diff_note.rb67
-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/services/check_gcp_project_billing_service.rb11
-rw-r--r--app/uploaders/object_storage.rb2
-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/groups/edit.html.haml109
-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/projects/clusters/gcp/_form.html.haml33
-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/empty_states/_wikis.html.haml30
-rw-r--r--app/views/shared/empty_states/_wikis_layout.html.haml7
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml2
-rw-r--r--app/views/shared/milestones/_top.html.haml2
-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
-rw-r--r--changelogs/unreleased/38759-fetch-available-parameters-directly-from-gke-when-creating-a-cluster.yml5
-rw-r--r--changelogs/unreleased/38919-wiki-empty-states.yml5
-rw-r--r--changelogs/unreleased/45190-create-notes-diff-files.yml5
-rw-r--r--changelogs/unreleased/46846-update-redis-namespace-to-1-6-0.yml5
-rw-r--r--changelogs/unreleased/ab-45389-remove-double-checked-internal-id-generation.yml5
-rw-r--r--changelogs/unreleased/add-artifacts_expire_at-to-api.yml5
-rw-r--r--changelogs/unreleased/add-background-migration-to-fill-file-store.yml5
-rw-r--r--changelogs/unreleased/bvl-add-username-to-terms-message.yml5
-rw-r--r--changelogs/unreleased/dz-redesign-group-settings-page.yml5
-rw-r--r--changelogs/unreleased/ensure-remote-mirror-columns-in-ce.yml5
-rw-r--r--changelogs/unreleased/groups-controller-show-performance.yml5
-rw-r--r--changelogs/unreleased/ignore-writing-trace-if-it-already-archived.yml5
-rw-r--r--changelogs/unreleased/patch-28.yml5
-rw-r--r--changelogs/unreleased/sh-fix-admin-page-counts-take-2.yml5
-rw-r--r--changelogs/unreleased/sh-tag-queue-duration-api-calls.yml5
-rw-r--r--config/sidekiq_queues.yml1
-rw-r--r--db/migrate/20180515121227_create_notes_diff_files.rb21
-rw-r--r--db/migrate/20180524132016_merge_requests_target_id_iid_state_partial_index.rb27
-rw-r--r--db/migrate/20180529093006_ensure_remote_mirror_columns.rb24
-rw-r--r--db/post_migrate/20180424151928_fill_file_store.rb72
-rw-r--r--db/schema.rb18
-rw-r--r--doc/administration/high_availability/database.md11
-rw-r--r--doc/administration/high_availability/gitlab.md35
-rw-r--r--doc/administration/integration/terminal.md16
-rw-r--r--doc/administration/monitoring/prometheus/index.md13
-rw-r--r--doc/api/jobs.md5
-rw-r--r--doc/api/settings.md2
-rw-r--r--doc/ci/autodeploy/index.md130
-rw-r--r--doc/ci/environments.md7
-rw-r--r--doc/ci/variables/README.md4
-rw-r--r--doc/development/fe_guide/icons.md114
-rw-r--r--doc/development/fe_guide/index.md4
-rw-r--r--doc/development/fe_guide/vue.md190
-rw-r--r--doc/development/new_fe_guide/development/accessibility.md49
-rw-r--r--doc/user/project/integrations/img/kubernetes_configuration.pngbin14407 -> 0 bytes
-rw-r--r--doc/user/project/integrations/kubernetes.md138
-rw-r--r--doc/user/project/integrations/project_services.md1
-rw-r--r--lib/api/api.rb3
-rw-r--r--lib/api/entities.rb1
-rw-r--r--lib/api/helpers/internal_helpers.rb6
-rw-r--r--lib/api/internal.rb6
-rw-r--r--lib/api/runner.rb5
-rw-r--r--lib/api/settings.rb2
-rw-r--r--lib/backup/repository.rb56
-rw-r--r--lib/banzai/reference_parser/issue_parser.rb3
-rw-r--r--lib/gitlab/auth/user_access_denied_reason.rb8
-rw-r--r--lib/gitlab/background_migration/fill_file_store_job_artifact.rb20
-rw-r--r--lib/gitlab/background_migration/fill_file_store_lfs_object.rb20
-rw-r--r--lib/gitlab/background_migration/fill_store_upload.rb21
-rw-r--r--lib/gitlab/database/count.rb72
-rw-r--r--lib/gitlab/diff/file.rb7
-rw-r--r--lib/gitlab/gitaly_client/storage_service.rb15
-rw-r--r--lib/gitlab/grape_logging/loggers/queue_duration_logger.rb26
-rw-r--r--lib/google_api/cloud_platform/client.rb17
-rw-r--r--locale/gitlab.pot45
-rw-r--r--package.json2
-rw-r--r--qa/qa/page/project/settings/ci_cd.rb4
-rw-r--r--qa/qa/specs/features/api/users_spec.rb9
-rw-r--r--spec/controllers/projects/clusters/gcp_controller_spec.rb34
-rw-r--r--spec/features/groups/group_settings_spec.rb21
-rw-r--r--spec/features/groups/share_lock_spec.rb31
-rw-r--r--spec/features/groups_spec.rb6
-rw-r--r--spec/features/projects/clusters/gcp_spec.rb191
-rw-r--r--spec/features/projects/user_sees_sidebar_spec.rb12
-rw-r--r--spec/features/projects/wiki/markdown_preview_spec.rb1
-rw-r--r--spec/features/projects/wiki/user_creates_wiki_page_spec.rb1
-rw-r--r--spec/features/projects/wiki/user_updates_wiki_page_spec.rb1
-rw-r--r--spec/features/projects/wiki/user_views_wiki_empty_spec.rb75
-rw-r--r--spec/features/projects/wiki/user_views_wiki_page_spec.rb2
-rw-r--r--spec/features/uploads/user_uploads_avatar_to_group_spec.rb4
-rw-r--r--spec/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown_spec.js103
-rw-r--r--spec/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown_spec.js92
-rw-r--r--spec/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown_spec.js88
-rw-r--r--spec/javascripts/projects/gke_cluster_dropdowns/helpers.js49
-rw-r--r--spec/javascripts/projects/gke_cluster_dropdowns/mock_data.js75
-rw-r--r--spec/javascripts/projects/gke_cluster_dropdowns/stores/actions_spec.js131
-rw-r--r--spec/javascripts/projects/gke_cluster_dropdowns/stores/getters_spec.js65
-rw-r--r--spec/javascripts/projects/gke_cluster_dropdowns/stores/mutations_spec.js87
-rw-r--r--spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js69
-rw-r--r--spec/javascripts/vue_shared/components/dropdown/dropdown_hidden_input_spec.js (renamed from spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js)8
-rw-r--r--spec/javascripts/vue_shared/components/dropdown/dropdown_search_input_spec.js52
-rw-r--r--spec/javascripts/vue_shared/components/dropdown/mock_data.js11
-rw-r--r--spec/lib/backup/repository_spec.rb34
-rw-r--r--spec/lib/gitlab/auth/user_access_denied_reason_spec.rb3
-rw-r--r--spec/lib/gitlab/database/count_spec.rb81
-rw-r--r--spec/lib/gitlab/diff/file_spec.rb54
-rw-r--r--spec/lib/gitlab/git_access_spec.rb2
-rw-r--r--spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb35
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/google_api/cloud_platform/client_spec.rb24
-rw-r--r--spec/migrations/fill_file_store_spec.rb43
-rw-r--r--spec/models/concerns/discussion_on_diff_spec.rb2
-rw-r--r--spec/models/diff_note_spec.rb62
-rw-r--r--spec/models/internal_id_spec.rb37
-rw-r--r--spec/models/note_diff_file_spec.rb11
-rw-r--r--spec/requests/api/helpers_spec.rb2
-rw-r--r--spec/requests/api/internal_spec.rb12
-rw-r--r--spec/requests/api/jobs_spec.rb8
-rw-r--r--spec/requests/api/runner_spec.rb17
-rw-r--r--spec/requests/lfs_http_spec.rb2
-rw-r--r--spec/services/check_gcp_project_billing_service_spec.rb32
-rw-r--r--spec/services/notes/create_service_spec.rb82
-rw-r--r--spec/uploaders/object_storage_spec.rb2
-rw-r--r--spec/views/admin/dashboard/index.html.haml_spec.rb5
-rw-r--r--spec/workers/check_gcp_project_billing_worker_spec.rb116
-rw-r--r--spec/workers/create_note_diff_file_worker_spec.rb16
-rw-r--r--yarn.lock313
165 files changed, 3628 insertions, 1589 deletions
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 383d13656e2..64470a1f087 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -181,7 +181,7 @@ Team labels specify what team is responsible for this issue.
Assigning a team label makes sure issues get the attention of the appropriate
people.
-The current team labels are ~Build, ~"CI/CD", ~Discussion, ~Documentation, ~Quality,
+The current team labels are ~Distribution, ~"CI/CD", ~Discussion, ~Documentation, ~Quality,
~Geo, ~Gitaly, ~Monitoring, ~Platform, ~Release, ~"Security Products" and ~"UX".
The descriptions on the [labels page][labels-page] explain what falls under the
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 7bb21aff834..89eba2c5b85 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.102.0
+0.103.0
diff --git a/Gemfile b/Gemfile
index cc0758f7c57..b6b82bad8a4 100644
--- a/Gemfile
+++ b/Gemfile
@@ -162,7 +162,7 @@ gem 'acts-as-taggable-on', '~> 5.0'
# Background jobs
gem 'sidekiq', '~> 5.1'
gem 'sidekiq-cron', '~> 0.6.0'
-gem 'redis-namespace', '~> 1.5.2'
+gem 'redis-namespace', '~> 1.6.0'
gem 'sidekiq-limit_fetch', '~> 3.4', require: false
# Cron Parser
@@ -412,7 +412,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 0.99.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 0.100.0', require: 'gitaly'
gem 'grpc', '~> 1.11.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
diff --git a/Gemfile.lock b/Gemfile.lock
index 7332b55c175..b0b7bb537a8 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -281,7 +281,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gitaly-proto (0.99.0)
+ gitaly-proto (0.100.0)
google-protobuf (~> 3.1)
grpc (~> 1.10)
github-linguist (5.3.3)
@@ -709,8 +709,8 @@ GEM
redis-activesupport (5.0.4)
activesupport (>= 3, < 6)
redis-store (>= 1.3, < 2)
- redis-namespace (1.5.2)
- redis (~> 3.0, >= 3.0.4)
+ redis-namespace (1.6.0)
+ redis (>= 3.0.4)
redis-rack (2.0.4)
rack (>= 1.5, < 3)
redis-store (>= 1.2, < 2)
@@ -918,7 +918,7 @@ GEM
unf (0.1.4)
unf_ext
unf_ext (0.0.7.5)
- unicode-display_width (1.3.0)
+ unicode-display_width (1.3.2)
unicorn (5.1.0)
kgio (~> 2.6)
raindrops (~> 0.7)
@@ -1036,7 +1036,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 0.99.0)
+ gitaly-proto (~> 0.100.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
@@ -1129,7 +1129,7 @@ DEPENDENCIES
recaptcha (~> 3.0)
redcarpet (~> 3.4)
redis (~> 3.2)
- redis-namespace (~> 1.5.2)
+ redis-namespace (~> 1.6.0)
redis-rails (~> 5.0.2)
request_store (~> 1.3)
responders (~> 2.0)
diff --git a/app/assets/javascripts/jobs/components/sidebar_details_block.vue b/app/assets/javascripts/jobs/components/sidebar_details_block.vue
index 916fa0d292d..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 float-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/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/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/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..6be39702546
--- /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="help-block"
+ :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..f813a4625d6
--- /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="help-block"
+ :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..0c63ff5dc63
--- /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="help-block"
+ :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/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/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 0188a55cbf3..f85f66b9c0b 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -1,3 +1,30 @@
+@mixin flat-connector-before($length: 44px) {
+ &::before {
+ content: '';
+ position: absolute;
+ top: 48%;
+ left: -$length;
+ border-top: 2px solid $border-color;
+ width: $length;
+ height: 1px;
+ }
+}
+
+@mixin build-content($border-radius: 30px) {
+ display: inline-block;
+ padding: 8px 10px 9px;
+ width: 100%;
+ border: 1px solid $border-color;
+ border-radius: $border-radius;
+ background-color: $white-light;
+
+ &:hover {
+ background-color: $stage-hover-bg;
+ border: 1px solid $dropdown-toggle-active-border-color;
+ color: $gl-text-color;
+ }
+}
+
.pipelines {
.stage {
max-width: 90px;
@@ -357,14 +384,8 @@
&:not(:first-child) {
margin-left: 44px;
- .left-connector::before {
- content: '';
- position: absolute;
- top: 48%;
- left: -44px;
- border-top: 2px solid $border-color;
- width: 44px;
- height: 1px;
+ .left-connector {
+ @include flat-connector-before;
}
}
}
@@ -479,12 +500,7 @@
}
.build-content {
- display: inline-block;
- padding: 8px 10px 9px;
- width: 100%;
- border: 1px solid $border-color;
- border-radius: 30px;
- background-color: $white-light;
+ @include build-content();
}
a.build-content:hover,
@@ -622,8 +638,7 @@
}
}
-// Dropdown button in mini pipeline graph
-button.mini-pipeline-graph-dropdown-toggle {
+@mixin mini-pipeline-item() {
border-radius: 100px;
background-color: $white-light;
border-width: 1px;
@@ -636,30 +651,6 @@ button.mini-pipeline-graph-dropdown-toggle {
position: relative;
vertical-align: middle;
- > .fa.fa-caret-down {
- position: absolute;
- left: 20px;
- top: 5px;
- display: inline-block;
- visibility: hidden;
- opacity: 0;
- color: inherit;
- font-size: 12px;
- transition: visibility 0.1s, opacity 0.1s linear;
- }
-
- &:active,
- &:focus,
- &:hover {
- outline: none;
- width: 35px;
-
- .fa.fa-caret-down {
- visibility: visible;
- opacity: 1;
- }
- }
-
// Dropdown button animation in mini pipeline graph
&.ci-status-icon-success {
@include mini-pipeline-graph-color($green-100, $green-500, $green-600);
@@ -691,6 +682,35 @@ button.mini-pipeline-graph-dropdown-toggle {
}
}
+// Dropdown button in mini pipeline graph
+button.mini-pipeline-graph-dropdown-toggle {
+ @include mini-pipeline-item();
+
+ > .fa.fa-caret-down {
+ position: absolute;
+ left: 20px;
+ top: 5px;
+ display: inline-block;
+ visibility: hidden;
+ opacity: 0;
+ color: inherit;
+ font-size: 12px;
+ transition: visibility 0.1s, opacity 0.1s linear;
+ }
+
+ &:active,
+ &:focus,
+ &:hover {
+ outline: none;
+ width: 35px;
+
+ .fa.fa-caret-down {
+ visibility: visible;
+ opacity: 1;
+ }
+ }
+}
+
/**
Action icons inside dropdowns:
- mini graph in pipelines table
@@ -744,7 +764,7 @@ button.mini-pipeline-graph-dropdown-toggle {
}
}
- // SVGs in the commit widget and mr widget
+ // SVGs in the commit widget and mr widget
a.ci-action-icon-container.ci-action-icon-wrapper svg {
top: 2px;
}
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/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/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/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/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/internal_id.rb b/app/models/internal_id.rb
index 189942c5ad8..f7f930e86ed 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/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/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/_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/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/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/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml
index 5739a57dcfe..be58a844f70 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.help-block &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/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/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/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index b8c3b3e4aa5..becd1c4884e 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -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/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
diff --git a/changelogs/unreleased/38759-fetch-available-parameters-directly-from-gke-when-creating-a-cluster.yml b/changelogs/unreleased/38759-fetch-available-parameters-directly-from-gke-when-creating-a-cluster.yml
new file mode 100644
index 00000000000..e7d0d37becd
--- /dev/null
+++ b/changelogs/unreleased/38759-fetch-available-parameters-directly-from-gke-when-creating-a-cluster.yml
@@ -0,0 +1,5 @@
+---
+title: Dynamically fetch GCP cluster creation parameters.
+merge_request: 17806
+author:
+type: changed
diff --git a/changelogs/unreleased/38919-wiki-empty-states.yml b/changelogs/unreleased/38919-wiki-empty-states.yml
new file mode 100644
index 00000000000..953fa29e659
--- /dev/null
+++ b/changelogs/unreleased/38919-wiki-empty-states.yml
@@ -0,0 +1,5 @@
+---
+title: Add helpful messages to empty wiki view
+merge_request: 19007
+author:
+type: other
diff --git a/changelogs/unreleased/45190-create-notes-diff-files.yml b/changelogs/unreleased/45190-create-notes-diff-files.yml
new file mode 100644
index 00000000000..efe322b682d
--- /dev/null
+++ b/changelogs/unreleased/45190-create-notes-diff-files.yml
@@ -0,0 +1,5 @@
+---
+title: Persist truncated note diffs on a new table
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/46846-update-redis-namespace-to-1-6-0.yml b/changelogs/unreleased/46846-update-redis-namespace-to-1-6-0.yml
new file mode 100644
index 00000000000..3707ad74b8f
--- /dev/null
+++ b/changelogs/unreleased/46846-update-redis-namespace-to-1-6-0.yml
@@ -0,0 +1,5 @@
+---
+title: Update redis-namespace to 1.6.0
+merge_request: 19166
+author: Takuya Noguchi
+type: other
diff --git a/changelogs/unreleased/ab-45389-remove-double-checked-internal-id-generation.yml b/changelogs/unreleased/ab-45389-remove-double-checked-internal-id-generation.yml
new file mode 100644
index 00000000000..d87604de0f8
--- /dev/null
+++ b/changelogs/unreleased/ab-45389-remove-double-checked-internal-id-generation.yml
@@ -0,0 +1,5 @@
+---
+title: Remove double-checked internal id generation.
+merge_request: 19181
+author:
+type: performance
diff --git a/changelogs/unreleased/add-artifacts_expire_at-to-api.yml b/changelogs/unreleased/add-artifacts_expire_at-to-api.yml
new file mode 100644
index 00000000000..7fe0d8b5720
--- /dev/null
+++ b/changelogs/unreleased/add-artifacts_expire_at-to-api.yml
@@ -0,0 +1,5 @@
+---
+title: Expose artifacts_expire_at field for job entity in api
+merge_request: 18872
+author: Semyon Pupkov
+type: added
diff --git a/changelogs/unreleased/add-background-migration-to-fill-file-store.yml b/changelogs/unreleased/add-background-migration-to-fill-file-store.yml
new file mode 100644
index 00000000000..ab6bde86fd4
--- /dev/null
+++ b/changelogs/unreleased/add-background-migration-to-fill-file-store.yml
@@ -0,0 +1,5 @@
+---
+title: Add backgound migration for filling nullfied file_store columns
+merge_request: 18557
+author:
+type: performance
diff --git a/changelogs/unreleased/bvl-add-username-to-terms-message.yml b/changelogs/unreleased/bvl-add-username-to-terms-message.yml
new file mode 100644
index 00000000000..b95d87e9265
--- /dev/null
+++ b/changelogs/unreleased/bvl-add-username-to-terms-message.yml
@@ -0,0 +1,5 @@
+---
+title: Add username to terms message in git and API calls
+merge_request: 19126
+author:
+type: changed
diff --git a/changelogs/unreleased/dz-redesign-group-settings-page.yml b/changelogs/unreleased/dz-redesign-group-settings-page.yml
new file mode 100644
index 00000000000..4a8dfbb61dc
--- /dev/null
+++ b/changelogs/unreleased/dz-redesign-group-settings-page.yml
@@ -0,0 +1,5 @@
+---
+title: Redesign group settings page into expandable sections
+merge_request: 19184
+author:
+type: changed
diff --git a/changelogs/unreleased/ensure-remote-mirror-columns-in-ce.yml b/changelogs/unreleased/ensure-remote-mirror-columns-in-ce.yml
new file mode 100644
index 00000000000..7617412431f
--- /dev/null
+++ b/changelogs/unreleased/ensure-remote-mirror-columns-in-ce.yml
@@ -0,0 +1,5 @@
+---
+title: Fix remote mirror database inconsistencies when upgrading from EE to CE
+merge_request: 19196
+author:
+type: fixed
diff --git a/changelogs/unreleased/groups-controller-show-performance.yml b/changelogs/unreleased/groups-controller-show-performance.yml
new file mode 100644
index 00000000000..bab54cc455e
--- /dev/null
+++ b/changelogs/unreleased/groups-controller-show-performance.yml
@@ -0,0 +1,5 @@
+---
+title: Improve performance of GroupsController#show
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/ignore-writing-trace-if-it-already-archived.yml b/changelogs/unreleased/ignore-writing-trace-if-it-already-archived.yml
new file mode 100644
index 00000000000..5c342e2a131
--- /dev/null
+++ b/changelogs/unreleased/ignore-writing-trace-if-it-already-archived.yml
@@ -0,0 +1,5 @@
+---
+title: Disallow updating job status if the job is not running
+merge_request: 19101
+author:
+type: fixed
diff --git a/changelogs/unreleased/patch-28.yml b/changelogs/unreleased/patch-28.yml
new file mode 100644
index 00000000000..1bbca138cae
--- /dev/null
+++ b/changelogs/unreleased/patch-28.yml
@@ -0,0 +1,5 @@
+---
+title: Fix FreeBSD can not upload artifacts due to wrong tmp path
+merge_request: 19148
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-admin-page-counts-take-2.yml b/changelogs/unreleased/sh-fix-admin-page-counts-take-2.yml
new file mode 100644
index 00000000000..d9bd1af9380
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-admin-page-counts-take-2.yml
@@ -0,0 +1,5 @@
+---
+title: Fix admin counters not working when PostgreSQL has secondaries
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-tag-queue-duration-api-calls.yml b/changelogs/unreleased/sh-tag-queue-duration-api-calls.yml
new file mode 100644
index 00000000000..686cceaab62
--- /dev/null
+++ b/changelogs/unreleased/sh-tag-queue-duration-api-calls.yml
@@ -0,0 +1,5 @@
+---
+title: Log Workhorse queue duration for Grape API calls
+merge_request:
+author:
+type: other
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index e1e8f36b663..d16060e8f45 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -75,4 +75,5 @@
- [pipeline_background, 1]
- [repository_update_remote_mirror, 1]
- [repository_remove_remote, 1]
+ - [create_note_diff_file, 1]
diff --git a/db/migrate/20180515121227_create_notes_diff_files.rb b/db/migrate/20180515121227_create_notes_diff_files.rb
new file mode 100644
index 00000000000..7108bc1a64b
--- /dev/null
+++ b/db/migrate/20180515121227_create_notes_diff_files.rb
@@ -0,0 +1,21 @@
+class CreateNotesDiffFiles < ActiveRecord::Migration
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def change
+ create_table :note_diff_files do |t|
+ t.references :diff_note, references: :notes, null: false, index: { unique: true }
+ t.text :diff, null: false
+ t.boolean :new_file, null: false
+ t.boolean :renamed_file, null: false
+ t.boolean :deleted_file, null: false
+ t.string :a_mode, null: false
+ t.string :b_mode, null: false
+ t.text :new_path, null: false
+ t.text :old_path, null: false
+ end
+
+ add_foreign_key :note_diff_files, :notes, column: :diff_note_id, on_delete: :cascade
+ end
+end
diff --git a/db/migrate/20180524132016_merge_requests_target_id_iid_state_partial_index.rb b/db/migrate/20180524132016_merge_requests_target_id_iid_state_partial_index.rb
new file mode 100644
index 00000000000..cee576b91c8
--- /dev/null
+++ b/db/migrate/20180524132016_merge_requests_target_id_iid_state_partial_index.rb
@@ -0,0 +1,27 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MergeRequestsTargetIdIidStatePartialIndex < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ INDEX_NAME = 'index_merge_requests_on_target_project_id_and_iid_opened'
+
+ disable_ddl_transaction!
+
+ def up
+ # On GitLab.com this index will take up roughly 5 MB of space.
+ add_concurrent_index(
+ :merge_requests,
+ [:target_project_id, :iid],
+ where: "state = 'opened'",
+ name: INDEX_NAME
+ )
+ end
+
+ def down
+ remove_concurrent_index_by_name(:merge_requests, INDEX_NAME)
+ end
+end
diff --git a/db/migrate/20180529093006_ensure_remote_mirror_columns.rb b/db/migrate/20180529093006_ensure_remote_mirror_columns.rb
new file mode 100644
index 00000000000..290416cb61c
--- /dev/null
+++ b/db/migrate/20180529093006_ensure_remote_mirror_columns.rb
@@ -0,0 +1,24 @@
+class EnsureRemoteMirrorColumns < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column :remote_mirrors, :last_update_started_at, :datetime unless column_exists?(:remote_mirrors, :last_update_started_at)
+ add_column :remote_mirrors, :remote_name, :string unless column_exists?(:remote_mirrors, :remote_name)
+
+ unless column_exists?(:remote_mirrors, :only_protected_branches)
+ add_column_with_default(:remote_mirrors,
+ :only_protected_branches,
+ :boolean,
+ default: false,
+ allow_null: false)
+ end
+ end
+
+ def down
+ # db/migrate/20180503131624_create_remote_mirrors.rb will remove the table
+ end
+end
diff --git a/db/post_migrate/20180424151928_fill_file_store.rb b/db/post_migrate/20180424151928_fill_file_store.rb
new file mode 100644
index 00000000000..b41feb233be
--- /dev/null
+++ b/db/post_migrate/20180424151928_fill_file_store.rb
@@ -0,0 +1,72 @@
+class FillFileStore < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class JobArtifact < ActiveRecord::Base
+ include EachBatch
+ self.table_name = 'ci_job_artifacts'
+ BATCH_SIZE = 10_000
+
+ def self.params_for_background_migration
+ yield self.where(file_store: nil), 'FillFileStoreJobArtifact', 5.minutes, BATCH_SIZE
+ end
+ end
+
+ class LfsObject < ActiveRecord::Base
+ include EachBatch
+ self.table_name = 'lfs_objects'
+ BATCH_SIZE = 10_000
+
+ def self.params_for_background_migration
+ yield self.where(file_store: nil), 'FillFileStoreLfsObject', 5.minutes, BATCH_SIZE
+ end
+ end
+
+ class Upload < ActiveRecord::Base
+ include EachBatch
+ self.table_name = 'uploads'
+ self.inheritance_column = :_type_disabled # Disable STI
+ BATCH_SIZE = 10_000
+
+ def self.params_for_background_migration
+ yield self.where(store: nil), 'FillStoreUpload', 5.minutes, BATCH_SIZE
+ end
+ end
+
+ def up
+ # NOTE: Schedule background migrations that fill 'NULL' value by '1'(ObjectStorage::Store::LOCAL) on `file_store`, `store` columns
+ #
+ # Here are the target columns
+ # - ci_job_artifacts.file_store
+ # - lfs_objects.file_store
+ # - uploads.store
+
+ FillFileStore::JobArtifact.params_for_background_migration do |relation, class_name, delay_interval, batch_size|
+ queue_background_migration_jobs_by_range_at_intervals(relation,
+ class_name,
+ delay_interval,
+ batch_size: batch_size)
+ end
+
+ FillFileStore::LfsObject.params_for_background_migration do |relation, class_name, delay_interval, batch_size|
+ queue_background_migration_jobs_by_range_at_intervals(relation,
+ class_name,
+ delay_interval,
+ batch_size: batch_size)
+ end
+
+ FillFileStore::Upload.params_for_background_migration do |relation, class_name, delay_interval, batch_size|
+ queue_background_migration_jobs_by_range_at_intervals(relation,
+ class_name,
+ delay_interval,
+ batch_size: batch_size)
+ end
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 37d336b9928..932b7f8da02 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20180521171529) do
+ActiveRecord::Schema.define(version: 20180529093006) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -1232,6 +1232,7 @@ ActiveRecord::Schema.define(version: 20180521171529) do
add_index "merge_requests", ["source_project_id", "source_branch"], name: "index_merge_requests_on_source_project_id_and_source_branch", using: :btree
add_index "merge_requests", ["target_branch"], name: "index_merge_requests_on_target_branch", using: :btree
add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid", unique: true, using: :btree
+ add_index "merge_requests", ["target_project_id", "iid"], name: "index_merge_requests_on_target_project_id_and_iid_opened", where: "((state)::text = 'opened'::text)", using: :btree
add_index "merge_requests", ["target_project_id", "merge_commit_sha", "id"], name: "index_merge_requests_on_tp_id_and_merge_commit_sha_and_id", using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title", using: :btree
add_index "merge_requests", ["title"], name: "index_merge_requests_on_title_trigram", using: :gin, opclasses: {"title"=>"gin_trgm_ops"}
@@ -1302,6 +1303,20 @@ ActiveRecord::Schema.define(version: 20180521171529) do
add_index "namespaces", ["runners_token"], name: "index_namespaces_on_runners_token", unique: true, using: :btree
add_index "namespaces", ["type"], name: "index_namespaces_on_type", using: :btree
+ create_table "note_diff_files", force: :cascade do |t|
+ t.integer "diff_note_id", null: false
+ t.text "diff", null: false
+ t.boolean "new_file", null: false
+ t.boolean "renamed_file", null: false
+ t.boolean "deleted_file", null: false
+ t.string "a_mode", null: false
+ t.string "b_mode", null: false
+ t.text "new_path", null: false
+ t.text "old_path", null: false
+ end
+
+ add_index "note_diff_files", ["diff_note_id"], name: "index_note_diff_files_on_diff_note_id", unique: true, using: :btree
+
create_table "notes", force: :cascade do |t|
t.text "note"
t.string "noteable_type"
@@ -2243,6 +2258,7 @@ ActiveRecord::Schema.define(version: 20180521171529) do
add_foreign_key "merge_requests_closing_issues", "merge_requests", on_delete: :cascade
add_foreign_key "milestones", "namespaces", column: "group_id", name: "fk_95650a40d4", on_delete: :cascade
add_foreign_key "milestones", "projects", name: "fk_9bd0a0c791", on_delete: :cascade
+ add_foreign_key "note_diff_files", "notes", column: "diff_note_id", on_delete: :cascade
add_foreign_key "notes", "projects", name: "fk_99e097b079", on_delete: :cascade
add_foreign_key "oauth_openid_requests", "oauth_access_grants", column: "access_grant_id", name: "fk_oauth_openid_requests_oauth_access_grants_access_grant_id"
add_foreign_key "pages_domains", "projects", name: "fk_ea2f6dfc6f", on_delete: :cascade
diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md
index ca6d8d2de67..b5124b1d540 100644
--- a/doc/administration/high_availability/database.md
+++ b/doc/administration/high_availability/database.md
@@ -33,16 +33,7 @@ If you use a cloud-managed service, or provide your own PostgreSQL:
external_url 'https://gitlab.example.com'
# Disable all components except PostgreSQL
- postgresql['enable'] = true
- bootstrap['enable'] = false
- nginx['enable'] = false
- unicorn['enable'] = false
- sidekiq['enable'] = false
- redis['enable'] = false
- prometheus['enable'] = false
- gitaly['enable'] = false
- gitlab_workhorse['enable'] = false
- mailroom['enable'] = false
+ roles ['postgres_role']
# PostgreSQL configuration
gitlab_rails['db_password'] = 'DB password'
diff --git a/doc/administration/high_availability/gitlab.md b/doc/administration/high_availability/gitlab.md
index e201848791c..0d9c10687f2 100644
--- a/doc/administration/high_availability/gitlab.md
+++ b/doc/administration/high_availability/gitlab.md
@@ -47,7 +47,8 @@ for each GitLab application server in your environment.
URL. Depending your the NFS configuration, you may need to change some GitLab
data locations. See [NFS documentation](nfs.md) for `/etc/gitlab/gitlab.rb`
configuration values for various scenarios. The example below assumes you've
- added NFS mounts in the default data locations.
+ added NFS mounts in the default data locations. Additionally the UID and GIDs
+ given are just examples and you should configure with your preferred values.
```ruby
external_url 'https://gitlab.example.com'
@@ -68,6 +69,14 @@ for each GitLab application server in your environment.
gitlab_rails['redis_port'] = '6379'
gitlab_rails['redis_host'] = '10.1.0.6' # IP/hostname of Redis server
gitlab_rails['redis_password'] = 'Redis Password'
+
+ # Ensure UIDs and GIDs match between servers for permissions via NFS
+ user['uid'] = 9000
+ user['gid'] = 9000
+ web_server['uid'] = 9001
+ web_server['gid'] = 9001
+ registry['uid'] = 9002
+ registry['gid'] = 9002
```
> **Note:** To maintain uniformity of links across HA clusters, the `external_url`
@@ -75,25 +84,24 @@ for each GitLab application server in your environment.
servers should point to the external url that users will use to access GitLab.
In a typical HA setup, this will be the url of the load balancer which will
route traffic to all GitLab application servers in the HA cluster.
-
-1. Run `sudo gitlab-ctl reconfigure` to compile the configuration.
+
+ > **Note:** When you specify `https` in the `external_url`, as in the example
+ above, GitLab assumes you have SSL certificates in `/etc/gitlab/ssl/`. If
+ certificates are not present, Nginx will fail to start. See
+ [Nginx documentation](http://docs.gitlab.com/omnibus/settings/nginx.html#enable-https)
+ for more information.
## First GitLab application server
-As a final step, run the setup rake task on the first GitLab application server.
-It is not necessary to run this on additional application servers.
+As a final step, run the setup rake task **only on** the first GitLab application server.
+Do not run this on additional application servers.
1. Initialize the database by running `sudo gitlab-rake gitlab:setup`.
+1. Run `sudo gitlab-ctl reconfigure` to compile the configuration.
> **WARNING:** Only run this setup task on **NEW** GitLab instances because it
will wipe any existing data.
-> **Note:** When you specify `https` in the `external_url`, as in the example
- above, GitLab assumes you have SSL certificates in `/etc/gitlab/ssl/`. If
- certificates are not present, Nginx will fail to start. See
- [Nginx documentation](http://docs.gitlab.com/omnibus/settings/nginx.html#enable-https)
- for more information.
-
## Extra configuration for additional GitLab application servers
Additional GitLab servers (servers configured **after** the first GitLab server)
@@ -101,8 +109,7 @@ need some extra configuration.
1. Configure shared secrets. These values can be obtained from the primary
GitLab server in `/etc/gitlab/gitlab-secrets.json`. Add these to
- `/etc/gitlab/gitlab.rb` **prior to** running the first `reconfigure` in
- the steps above.
+ `/etc/gitlab/gitlab.rb` **prior to** running the first `reconfigure`.
```ruby
gitlab_shell['secret_token'] = 'fbfb19c355066a9afb030992231c4a363357f77345edd0f2e772359e5be59b02538e1fa6cae8f93f7d23355341cea2b93600dab6d6c3edcdced558fc6d739860'
@@ -115,6 +122,8 @@ need some extra configuration.
from running on upgrade. Only the primary GitLab application server should
handle migrations.
+1. Run `sudo gitlab-ctl reconfigure` to compile the configuration.
+
## Troubleshooting
- `mount: wrong fs type, bad option, bad superblock on`
diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md
index 91e844c7b42..32ad63c3706 100644
--- a/doc/administration/integration/terminal.md
+++ b/doc/administration/integration/terminal.md
@@ -1,12 +1,13 @@
# Web terminals
-> [Introduced][ce-7690] in GitLab 8.15. Only project masters and owners can
- access web terminals.
+>
+[Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7690)
+in GitLab 8.15. Only project masters and owners can access web terminals.
-With the introduction of the [Kubernetes project service][kubservice], GitLab
-gained the ability to store and use credentials for a Kubernetes cluster. One
-of the things it uses these credentials for is providing access to
-[web terminals](../../ci/environments.html#web-terminals) for environments.
+With the introduction of the [Kubernetes integration](../../user/project/clusters/index.md),
+GitLab gained the ability to store and use credentials for a Kubernetes cluster.
+One of the things it uses these credentials for is providing access to
+[web terminals](../../ci/environments.md#web-terminals) for environments.
## How it works
@@ -80,6 +81,3 @@ Terminal sessions use long-lived connections; by default, these may last
forever. You can configure a maximum session time in the Admin area of your
GitLab instance if you find this undesirable from a scalability or security
point of view.
-
-[ce-7690]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/7690
-[kubservice]: ../../user/project/integrations/kubernetes.md
diff --git a/doc/administration/monitoring/prometheus/index.md b/doc/administration/monitoring/prometheus/index.md
index f47add48345..1c79e86dcb4 100644
--- a/doc/administration/monitoring/prometheus/index.md
+++ b/doc/administration/monitoring/prometheus/index.md
@@ -29,7 +29,8 @@ For installations from source you'll have to install and configure it yourself.
Prometheus and it's exporters are on by default, starting with GitLab 9.0.
Prometheus will run as the `gitlab-prometheus` user and listen on
-`http://localhost:9090`. Each exporter will be automatically be set up as a
+`http://localhost:9090`. By default Prometheus is only accessible from the GitLab server itself.
+Each exporter will be automatically set up as a
monitoring target for Prometheus, unless individually disabled.
To disable Prometheus and all of its exporters, as well as any added in the future:
@@ -44,14 +45,16 @@ To disable Prometheus and all of its exporters, as well as any added in the futu
1. Save the file and [reconfigure GitLab][reconfigure] for the changes to
take effect
-## Changing the port Prometheus listens on
+## Changing the port and address Prometheus listens on
>**Note:**
The following change was added in [GitLab Omnibus 8.17][1261]. Although possible,
-it's not recommended to change the default address and port Prometheus listens
+it's not recommended to change the port Prometheus listens
on as this might affect or conflict with other services running on the GitLab
server. Proceed at your own risk.
+In order to access Prometheus from outside the GitLab server you will need to
+set a FQDN or IP in `prometheus['listen_address']`.
To change the address/port that Prometheus listens on:
1. Edit `/etc/gitlab/gitlab.rb`
@@ -80,9 +83,9 @@ You can visit `http://localhost:9090` for the dashboard that Prometheus offers b
>**Note:**
If SSL has been enabled on your GitLab instance, you may not be able to access
-Prometheus on the same browser as GitLab due to [HSTS][hsts]. We plan to
+Prometheus on the same browser as GitLab if using the same FQDN due to [HSTS][hsts]. We plan to
[provide access via GitLab][multi-user-prometheus], but in the interim there are
-some workarounds: using a separate browser for Prometheus, resetting HSTS, or
+some workarounds: using a separate FQDN, using server IP, using a separate browser for Prometheus, resetting HSTS, or
having [Nginx proxy it][nginx-custom-config].
The performance data collected by Prometheus can be viewed directly in the
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
index e4e48edd9a7..0fbfc7cf0fd 100644
--- a/doc/api/jobs.md
+++ b/doc/api/jobs.md
@@ -38,6 +38,7 @@ Example of response
"size": 1000
},
"finished_at": "2015-12-24T17:54:27.895Z",
+ "artifacts_expire_at": "2016-01-23T17:54:27.895Z"
"id": 7,
"name": "teaspoon",
"pipeline": {
@@ -81,6 +82,7 @@ Example of response
"created_at": "2015-12-24T15:51:21.727Z",
"artifacts_file": null,
"finished_at": "2015-12-24T17:54:24.921Z",
+ "artifacts_expire_at": "2016-01-23T17:54:24.921Z",
"id": 6,
"name": "rspec:other",
"pipeline": {
@@ -152,6 +154,7 @@ Example of response
"size": 1000
},
"finished_at": "2015-12-24T17:54:27.895Z",
+ "artifacts_expire_at": "2016-01-23T17:54:27.895Z"
"id": 7,
"name": "teaspoon",
"pipeline": {
@@ -195,6 +198,7 @@ Example of response
"created_at": "2015-12-24T15:51:21.727Z",
"artifacts_file": null,
"finished_at": "2015-12-24T17:54:24.921Z",
+ "artifacts_expire_at": "2016-01-23T17:54:24.921Z"
"id": 6,
"name": "rspec:other",
"pipeline": {
@@ -261,6 +265,7 @@ Example of response
"created_at": "2015-12-24T15:51:21.880Z",
"artifacts_file": null,
"finished_at": "2015-12-24T17:54:31.198Z",
+ "artifacts_expire_at": "2016-01-23T17:54:31.198Z",
"id": 8,
"name": "rubocop",
"pipeline": {
diff --git a/doc/api/settings.md b/doc/api/settings.md
index e06b1bfb6df..1ebfe4924b1 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -133,7 +133,7 @@ PUT /application/settings
| `repository_checks_enabled` | boolean | no | GitLab will periodically run 'git fsck' in all project and wiki repositories to look for silent disk corruption issues. |
| `repository_storages` | array of strings | no | A list of names of enabled storage paths, taken from `gitlab.yml`. New projects will be created in one of these stores, chosen at random. |
| `require_two_factor_authentication` | boolean | no | Require all users to setup Two-factor authentication |
-| `restricted_visibility_levels` | array of strings | no | Selected levels cannot be used by non-admin users for projects or snippets. Can take `private`, `internal` and `public` as a parameter. Default is null which means there is no restriction. |
+| `restricted_visibility_levels` | array of strings | no | Selected levels cannot be used by non-admin users for groups, projects or snippets. Can take `private`, `internal` and `public` as a parameter. Default is null which means there is no restriction. |
| `rsa_key_restriction` | integer | no | The minimum allowed bit length of an uploaded RSA key. Default is `0` (no restriction). `-1` disables RSA keys. |
| `send_user_confirmation_email` | boolean | no | Send confirmation email on sign-up |
| `sentry_dsn` | string | yes (if `sentry_enabled` is true) | Sentry Data Source Name |
diff --git a/doc/ci/autodeploy/index.md b/doc/ci/autodeploy/index.md
index 7102af5c529..985ec4b972c 100644
--- a/doc/ci/autodeploy/index.md
+++ b/doc/ci/autodeploy/index.md
@@ -1,129 +1 @@
-# Auto Deploy
-
-> [Introduced][mr-8135] in GitLab 8.15.
-> Auto deploy is an experimental feature and is **not recommended for Production use** at this time.
-
-> As of GitLab 9.1, access to the container registry is only available while the
-Pipeline is running. Restarting a pod, scaling a service, or other actions which
-require on-going access **will fail**. On-going secure access is planned for a
-subsequent release.
-
-> As of GitLab 10.0, Auto Deploy templates are **deprecated** and the
-functionality has been included in [Auto
-DevOps](../../topics/autodevops/index.md).
-
-Auto deploy is an easy way to configure GitLab CI for the deployment of your
-application. GitLab Community maintains a list of `.gitlab-ci.yml`
-templates for various infrastructure providers and deployment scripts
-powering them. These scripts are responsible for packaging your application,
-setting up the infrastructure and spinning up necessary services (for
-example a database).
-
-## How it works
-
-The Autodeploy templates are based on the [kubernetes-deploy][kube-deploy]
-project which is used to simplify the deployment process to Kubernetes by
-providing intelligent `build`, `deploy`, and `destroy` commands which you can
-use in your `.gitlab-ci.yml` as is. It uses [Herokuish](https://github.com/gliderlabs/herokuish),
-which uses [Heroku buildpacks](https://devcenter.heroku.com/articles/buildpacks)
-to do some of the work, plus some of GitLab's own tools to package it all up. For
-your convenience, a [Docker image][kube-image] is also provided.
-
-You can use the [Kubernetes project service](../../user/project/integrations/kubernetes.md)
-to store credentials to your infrastructure provider and they will be available
-during the deployment.
-
-## Quick start
-
-We made a [simple guide](quick_start_guide.md) to using Auto Deploy with GitLab.com.
-
-For a demonstration of GitLab Auto Deploy, read the blog post [Auto Deploy from GitLab to an OpenShift Container Cluster](https://about.gitlab.com/2017/05/16/devops-containers-gitlab-openshift/)
-
-## Supported templates
-
-The list of supported auto deploy templates is available in the
-[gitlab-ci-yml project][auto-deploy-templates].
-
-## Configuration
-
->**Note:**
-In order to understand why the following steps are required, read the
-[how it works](#how-it-works) section.
-
-To configure Autodeploy, you will need to:
-
-1. Enable a deployment [project service][project-services] to store your
- credentials. For example, if you want to deploy to OpenShift you have to
- enable [Kubernetes service][kubernetes-service].
-1. Configure GitLab Runner to use the
- [Docker or Kubernetes executor](https://docs.gitlab.com/runner/executors/) with
- [privileged mode enabled][docker-in-docker].
-1. Navigate to the "Project" tab and click "Set up auto deploy" button.
- ![Auto deploy button](img/auto_deploy_button.png)
-1. Select a template.
- ![Dropdown with auto deploy templates](img/auto_deploy_dropdown.png)
-1. Commit your changes and create a merge request.
-1. Test your deployment configuration using a [Review App][review-app] that was
- created automatically for you.
-
-## Private project support
-
-> Experimental support [introduced][mr-2] in GitLab 9.1.
-
-When a project has been marked as private, GitLab's [Container Registry][container-registry] requires authentication when downloading containers. Auto deploy will automatically provide the required authentication information to Kubernetes, allowing temporary access to the registry. Authentication credentials will be valid while the pipeline is running, allowing for a successful initial deployment.
-
-After the pipeline completes, Kubernetes will no longer be able to access the container registry. Restarting a pod, scaling a service, or other actions which require on-going access to the registry will fail. On-going secure access is planned for a subsequent release.
-
-## PostgreSQL database support
-
-> Experimental support [introduced][mr-8] in GitLab 9.1.
-
-In order to support applications that require a database, [PostgreSQL][postgresql] is provisioned by default. Credentials to access the database are preconfigured, but can be customized by setting the associated [variables](#postgresql-variables). These credentials can be used for defining a `DATABASE_URL` of the format: `postgres://user:password@postgres-host:postgres-port/postgres-database`. It is important to note that the database itself is temporary, and contents will be not be saved.
-
-PostgreSQL provisioning can be disabled by setting the variable `DISABLE_POSTGRES` to `"yes"`.
-
-The following PostgreSQL variables are supported:
-
-1. `DISABLE_POSTGRES: "yes"`: disable automatic deployment of PostgreSQL
-1. `POSTGRES_USER: "my-user"`: use custom username for PostgreSQL
-1. `POSTGRES_PASSWORD: "password"`: use custom password for PostgreSQL
-1. `POSTGRES_DB: "my database"`: use custom database name for PostgreSQL
-
-## Auto Monitoring
-
-> Introduced in [GitLab 9.5](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13438).
-
-Apps auto-deployed using one the [Kubernetes templates](#supported-templates) can also be automatically monitored for:
-
-* Response Metrics: latency, throughput, error rate
-* System Metrics: CPU utilization, memory utilization
-
-Metrics are gathered from [nginx-ingress](../../user/project/integrations/prometheus_library/nginx_ingress.md) and [Kubernetes](../../user/project/integrations/prometheus_library/kubernetes.md).
-
-To view the metrics, open the [Monitoring dashboard for a deployed environment](../environments.md#monitoring-environments).
-
-![Auto Metrics](img/auto_monitoring.png)
-
-### Configuring Auto Monitoring
-
-If GitLab has been deployed using the [omnibus-gitlab](../../install/kubernetes/gitlab_omnibus.md) Helm chart, no configuration is required.
-
-If you have installed GitLab using a different method:
-
-1. [Deploy Prometheus](../../user/project/integrations/prometheus.md#configuring-your-own-prometheus-server-within-kubernetes) into your Kubernetes cluster
-1. If you would like response metrics, ensure you are running at least version 0.9.0 of NGINX Ingress and [enable Prometheus metrics](https://github.com/kubernetes/ingress/blob/master/examples/customization/custom-vts-metrics/nginx/nginx-vts-metrics-conf.yaml).
-1. Finally, [annotate](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) the NGINX Ingress deployment to be scraped by Prometheus using `prometheus.io/scrape: "true"` and `prometheus.io/port: "10254"`.
-
-[mr-8135]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/8135
-[mr-2]: https://gitlab.com/gitlab-examples/kubernetes-deploy/merge_requests/2
-[mr-8]: https://gitlab.com/gitlab-examples/kubernetes-deploy/merge_requests/8
-[project-settings]: https://docs.gitlab.com/ce/public_access/public_access.html
-[project-services]: ../../user/project/integrations/project_services.md
-[auto-deploy-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml/tree/master/autodeploy
-[kubernetes-service]: ../../user/project/integrations/kubernetes.md
-[docker-in-docker]: ../docker/using_docker_build.md#use-docker-in-docker-executor
-[review-app]: ../review_apps/index.md
-[kube-image]: https://gitlab.com/gitlab-examples/kubernetes-deploy/container_registry "Kubernetes deploy Container Registry"
-[kube-deploy]: https://gitlab.com/gitlab-examples/kubernetes-deploy "Kubernetes deploy example project"
-[container-registry]: https://docs.gitlab.com/ce/user/project/container_registry.html
-[postgresql]: https://www.postgresql.org/
+This document was moved to [another location](../../topics/autodevops/index.md#auto-deploy).
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index 3a491f0073c..0d54f375c93 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -24,7 +24,7 @@ Environments are like tags for your CI jobs, describing where code gets deployed
Deployments are created when [jobs] deploy versions of code to environments,
so every environment can have one or more deployments. GitLab keeps track of
your deployments, so you always know what is currently being deployed on your
-servers. If you have a deployment service such as [Kubernetes][kubernetes-service]
+servers. If you have a deployment service such as [Kubernetes][kube]
enabled for your project, you can use it to assist with your deployments, and
can even access a [web terminal](#web-terminals) for your environment from within GitLab!
@@ -605,7 +605,7 @@ Web terminals were added in GitLab 8.15 and are only available to project
masters and owners.
If you deploy to your environments with the help of a deployment service (e.g.,
-the [Kubernetes service][kubernetes-service]), GitLab can open
+the [Kubernetes integration][kube]), GitLab can open
a terminal session to your environment! This is a very powerful feature that
allows you to debug issues without leaving the comfort of your web browser. To
enable it, just follow the instructions given in the service integration
@@ -671,7 +671,6 @@ Below are some links you may find interesting:
[Pipelines]: pipelines.md
[jobs]: yaml/README.md#jobs
[yaml]: yaml/README.md
-[kubernetes-service]: ../user/project/integrations/kubernetes.md
[environments]: #environments
[deployments]: #deployments
[permissions]: ../user/permissions.md
@@ -683,5 +682,5 @@ Below are some links you may find interesting:
[gitlab-flow]: ../workflow/gitlab_flow.md
[gitlab runner]: https://docs.gitlab.com/runner/
[git-strategy]: yaml/README.md#git-strategy
-[kube]: ../user/project/integrations/kubernetes.md
+[kube]: ../user/project/clusters/index.md
[prom]: ../user/project/integrations/prometheus.md
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index aedf7958c8a..683846a536b 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -215,8 +215,8 @@ are set in the build environment. These variables are only defined for
[deployment jobs](../environments.md). Please consult the documentation of
the project services that you are using to learn which variables they define.
-An example project service that defines deployment variables is
-[Kubernetes Service](../../user/project/integrations/kubernetes.md#deployment-variables).
+An example project service that defines deployment variables is the
+[Kubernetes integration](../../user/project/clusters/index.md#deployment-variables).
## Debug tracing
diff --git a/doc/development/fe_guide/icons.md b/doc/development/fe_guide/icons.md
index b469a9c6aef..3d8da6accc1 100644
--- a/doc/development/fe_guide/icons.md
+++ b/doc/development/fe_guide/icons.md
@@ -1,26 +1,44 @@
-# Icons
+# Icons and SVG Illustrations
-We are using SVG Icons in GitLab with a SVG Sprite, due to this the icons are only loaded once and then referenced through an ID. The sprite SVG is located under `/assets/icons.svg`. Our goal is to replace one by one all inline SVG Icons (as those currently bloat the HTML) and also all Font Awesome usages.
+We manage our own Icon and Illustration library in the [gitlab-svgs][gitlab-svgs] repository.
+This repository is published on [npm][npm] and managed as a dependency via yarn.
+You can browse all available Icons and Illustrations [here][svg-preview].
+To upgrade to a new version run `yarn upgrade @gitlab-org/gitlab-svgs`.
-### Usage in HAML/Rails
+## Icons
-To use a sprite Icon in HAML or Rails we use a specific helper function :
+We are using SVG Icons in GitLab with a SVG Sprite.
+This means the icons are only loaded once, and are referenced through an ID.
+The sprite SVG is located under `/assets/icons.svg`.
+
+Our goal is to replace one by one all inline SVG Icons (as those currently bloat the HTML) and also all Font Awesome icons.
-`sprite_icon(icon_name, size: nil, css_class: '')`
+### Usage in HAML/Rails
-**icon_name** Use the icon_name that you can find in the SVG Sprite ([Overview is available here](http://gitlab-org.gitlab.io/gitlab-svgs/)`).
+To use a sprite Icon in HAML or Rails we use a specific helper function :
-**size (optional)** Use one of the following sizes : 16,24,32,48,72 (this will be translated into a `s16` class)
+```ruby
+sprite_icon(icon_name, size: nil, css_class: '')
+```
-**css_class (optional)** If you want to add additional css classes
+- **icon_name** Use the icon_name that you can find in the SVG Sprite
+ ([Overview is available here][svg-preview]).
+- **size (optional)** Use one of the following sizes : 16, 24, 32, 48, 72 (this will be translated into a `s16` class)
+- **css_class (optional)** If you want to add additional css classes
**Example**
-`= sprite_icon('issues', size: 72, css_class: 'icon-danger')`
+```haml
+= sprite_icon('issues', size: 72, css_class: 'icon-danger')
+```
**Output from example above**
-`<svg class="s72 icon-danger"><use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/assets/icons.svg#issues"></use></svg>`
+```html
+<svg class="s72 icon-danger">
+ <use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="/assets/icons.svg#issues"></use>
+</svg>
+```
### Usage in Vue
@@ -28,33 +46,71 @@ We have a special Vue component for our sprite icons in `\vue_shared\components\
Sample usage :
-`<icon
- name="retry"
- :size="32"
- css-classes="top"
- />`
-
-**name** Name of the Icon in the SVG Sprite ([Overview is available here](http://gitlab-org.gitlab.io/gitlab-svgs/)`).
-
-**size (optional)** Number value for the size which is then mapped to a specific CSS class (Available Sizes: 8,12,16,18,24,32,48,72 are mapped to `sXX` css classes)
-
-**css-classes (optional)** Additional CSS Classes to add to the svg tag.
+```javascript
+<script>
+import Icon from "~/vue_shared/components/icon.vue"
+
+export default {
+ components: {
+ Icon,
+ },
+};
+<script>
+<template>
+ <icon
+ name="issues"
+ :size="72"
+ css-classes="icon-danger"
+ />
+</template>
+```
+
+- **name** Name of the Icon in the SVG Sprite ([Overview is available here][svg-preview]).
+- **size (optional)** Number value for the size which is then mapped to a specific CSS class
+ (Available Sizes: 8, 12, 16, 18, 24, 32, 48, 72 are mapped to `sXX` css classes)
+- **css-classes (optional)** Additional CSS Classes to add to the svg tag.
### Usage in HTML/JS
-Please use the following function inside JS to render an icon :
+Please use the following function inside JS to render an icon:
`gl.utils.spriteIcon(iconName)`
-## Adding a new icon to the sprite
+## SVG Illustrations
-All Icons and Illustrations are managed in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository which is added as a dev-dependency.
+Please use from now on for any SVG based illustrations simple `img` tags to show an illustration by simply using either `image_tag` or `image_path` helpers.
+Please use the class `svg-content` around it to ensure nice rendering.
-To upgrade to a new SVG Sprite version run `yarn upgrade @gitlab-org/gitlab-svgs`.
+### Usage in HAML/Rails
-# SVG Illustrations
+**Example**
-Please use from now on for any SVG based illustrations simple `img` tags to show an illustration by simply using either `image_tag` or `image_path` helpers. Please use the class `svg-content` around it to ensure nice rendering. The illustrations are also organised in the [gitlab-svgs](https://gitlab.com/gitlab-org/gitlab-svgs) repository (as they are then automatically optimised).
+```haml
+.svg-content
+ = image_tag 'illustrations/merge_requests.svg'
+```
-**Example**
+### Usage in Vue
-`= image_tag 'illustrations/merge_requests.svg'`
+To use an SVG illustrations in a template provide the path as a property and display it through a standard img tag.
+
+Component:
+
+```js
+<script>
+export default {
+ props: {
+ svgIllustrationPath: {
+ type: String,
+ required: true,
+ },
+ },
+};
+<script>
+<template>
+ <img :src="svgIllustrationPath" />
+</template>
+```
+
+[npm]: https://www.npmjs.com/package/@gitlab-org/gitlab-svgs
+[gitlab-svgs]: https://gitlab.com/gitlab-org/gitlab-svgs
+[svg-preview]: https://gitlab-org.gitlab.io/gitlab-svgs
diff --git a/doc/development/fe_guide/index.md b/doc/development/fe_guide/index.md
index 6d3796e7560..11b9a2e6a64 100644
--- a/doc/development/fe_guide/index.md
+++ b/doc/development/fe_guide/index.md
@@ -54,8 +54,8 @@ Vuex specific design patterns and practices.
## [Axios](axios.md)
Axios specific practices and gotchas.
-## [Icons](icons.md)
-How we use SVG for our Icons.
+## [Icons and Illustrations](icons.md)
+How we use SVG for our Icons and Illustrations.
## [Components](components.md)
diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md
index f971d8b7388..e31ee087358 100644
--- a/doc/development/fe_guide/vue.md
+++ b/doc/development/fe_guide/vue.md
@@ -8,7 +8,7 @@ All new features built with Vue.js must follow a [Flux architecture][flux].
The main goal we are trying to achieve is to have only one data flow and only one data entry.
In order to achieve this goal, you can either use [vuex](#vuex) or use the [store pattern][state-management], explained below:
-Each Vue bundle needs a Store - where we keep all the data -,a Service - that we use to communicate with the server - and a main Vue component.
+Each Vue bundle needs a Store - where we keep all the data -, a Service - that we use to communicate with the server - and a main Vue component.
Think of the Main Vue Component as the entry point of your application. This is the only smart
component that should exist in each Vue feature.
@@ -17,7 +17,7 @@ This component is responsible for:
1. Calling the Store to store the data received
1. Mounting all the other components
- ![Vue Architecture](img/vue_arch.png)
+![Vue Architecture](img/vue_arch.png)
You can also read about this architecture in vue docs about [state management][state-management]
and about [one way data flow][one-way-data-flow].
@@ -51,14 +51,14 @@ of the new feature should be.
The Store and the Service should be imported and initialized in this file and
provided as a prop to the main component.
-Don't forget to follow [these steps.][page_specific_javascript]
+Don't forget to follow [these steps][page_specific_javascript].
### Bootstrapping Gotchas
-#### Providing data from Haml to JavaScript
+#### Providing data from HAML to JavaScript
While mounting a Vue application may be a need to provide data from Rails to JavaScript.
To do that, provide the data through `data` attributes in the HTML element and query them while mounting the application.
-_Note:_ You should only do this while initing the application, because the mounted element will be replaced with Vue-generated DOM.
+_Note:_ You should only do this while initializing the application, because the mounted element will be replaced with Vue-generated DOM.
The advantage of providing data from the DOM to the Vue instance through `props` in the `render` function
instead of querying the DOM inside the main vue component is that makes tests easier by avoiding the need to
@@ -68,6 +68,7 @@ create a fixture or an HTML element in the unit test. See the following example:
// haml
.js-vue-app{ data: { endpoint: 'foo' }}
+// index.js
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '.js-vue-app',
data() {
@@ -87,13 +88,11 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
```
#### Accessing the `gl` object
-When we need to query the `gl` object for data that won't change during the application's lyfecyle, we should do it in the same place where we query the DOM.
+When we need to query the `gl` object for data that won't change during the application's life cyle, we should do it in the same place where we query the DOM.
By following this practice, we can avoid the need to mock the `gl` object, which will make tests easier.
It should be done while initializing our Vue instance, and the data should be provided as `props` to the main component:
-##### example:
```javascript
-
document.addEventListener('DOMContentLoaded', () => new Vue({
el: '.js-vue-app',
render(createElement) {
@@ -121,25 +120,6 @@ in one table would not be a good use of this pattern.
You can read more about components in Vue.js site, [Component System][component-system]
-#### Components Gotchas
-1. Using SVGs icons in components: To use an SVG icon in a template use the `icon.vue`
-1. Using SVGs illustrations in components: To use an SVG illustrations in a template provide the path as a prop and display it through a standard img tag.
- ```javascript
- <script>
- export default {
- props: {
- svgIllustrationPath: {
- type: String,
- required: true,
- },
- },
- };
- <script>
- <template>
- <img :src="svgIllustrationPath" />
- </template>
- ```
-
### A folder for the Store
#### Vuex
@@ -163,13 +143,13 @@ Refer to [axios](axios.md) for more details.
Axios instance should only be imported in the service file.
- ```javascript
- import axios from 'javascripts/lib/utils/axios_utils';
- ```
+```javascript
+import axios from '~/lib/utils/axios_utils';
+```
### End Result
-The following example shows an application:
+The following example shows an application:
```javascript
// store.js
@@ -177,8 +157,8 @@ export default class Store {
/**
* This is where we will iniatialize the state of our data.
- * Usually in a small SPA you don't need any options when starting the store. In the case you do
- * need guarantee it's an Object and it's documented.
+ * Usually in a small SPA you don't need any options when starting the store.
+ * In that case you do need guarantee it's an Object and it's documented.
*
* @param {Object} options
*/
@@ -186,7 +166,7 @@ export default class Store {
this.options = options;
// Create a state object to handle all our data in the same place
- this.todos = []:
+ this.todos = [];
}
setTodos(todos = []) {
@@ -207,7 +187,7 @@ export default class Store {
}
// service.js
-import axios from 'javascripts/lib/utils/axios_utils'
+import axios from '~/lib/utils/axios_utils'
export default class Service {
constructor(options) {
@@ -233,8 +213,8 @@ export default {
type: Object,
required: true,
},
- }
-}
+ },
+};
</script>
<template>
<div>
@@ -275,7 +255,7 @@ export default {
},
created() {
- this.service = new Service('todos');
+ this.service = new Service('/todos');
this.getTodos();
},
@@ -284,9 +264,9 @@ export default {
getTodos() {
this.isLoading = true;
- this.service.getTodos()
- .then(response => response.json())
- .then((response) => {
+ this.service
+ .getTodos()
+ .then(response => {
this.store.setTodos(response);
this.isLoading = false;
})
@@ -296,18 +276,21 @@ export default {
});
},
- addTodo(todo) {
- this.service.addTodo(todo)
- then(response => response.json())
- .then((response) => {
- this.store.addTodo(response);
- })
- .catch(() => {
- // Show an error
- });
- }
- }
-}
+ addTodo(event) {
+ this.service
+ .addTodo({
+ title: 'New entry',
+ text: `You clicked on ${event.target.tagName}`,
+ })
+ .then(response => {
+ this.store.addTodo(response);
+ })
+ .catch(() => {
+ // Show an error
+ });
+ },
+ },
+};
</script>
<template>
<div class="container">
@@ -333,7 +316,7 @@ export default {
<div>
</template>
-// bundle.js
+// index.js
import todoComponent from 'todos_main_component.vue';
new Vue({
@@ -365,76 +348,79 @@ Each Vue component has a unique output. This output is always present in the ren
Although we can test each method of a Vue component individually, our goal must be to test the output
of the render/template function, which represents the state at all times.
-Make use of Vue Resource Interceptors to mock data returned by the service.
+Make use of the [axios mock adapter](axios.md#mock-axios-response-on-tests) to mock data returned.
Here's how we would test the Todo App above:
```javascript
-import component from 'todos_main_component';
+import Vue from 'vue';
+import axios from '~/lib/utils/axios_utils';
+import MockAdapter from 'axios-mock-adapter';
describe('Todos App', () => {
- it('should render the loading state while the request is being made', () => {
+ let vm;
+ let mock;
+
+ beforeEach(() => {
+ // Create a mock adapter for stubbing axios API requests
+ mock = new MockAdapter(axios);
+
const Component = Vue.extend(component);
- const vm = new Component().$mount();
+ // Mount the Component
+ vm = new Component().$mount();
+ });
+
+ afterEach(() => {
+ // Reset the mock adapter
+ mock.restore();
+ // Destroy the mounted component
+ vm.$destroy();
+ });
+ it('should render the loading state while the request is being made', () => {
expect(vm.$el.querySelector('i.fa-spin')).toBeDefined();
});
- describe('with data', () => {
- // Mock the service to return data
- const interceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([{
+ it('should render todos returned by the endpoint', done => {
+ // Mock the get request on the API endpoint to return data
+ mock.onGet('/todos').replyOnce(200, [
+ {
title: 'This is a todo',
- body: 'This is the text'
- }]), {
- status: 200,
- }));
- };
-
- let vm;
-
- beforeEach(() => {
- Vue.http.interceptors.push(interceptor);
-
- const Component = Vue.extend(component);
+ text: 'This is the text',
+ },
+ ]);
- vm = new Component().$mount();
+ Vue.nextTick(() => {
+ const items = vm.$el.querySelectorAll('.js-todo-list div')
+ expect(items.length).toBe(1);
+ expect(items[0].textContent).toContain('This is the text');
+ done();
});
+ });
- afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
- });
+ it('should add a todos on button click', (done) => {
+ // Mock the put request and check that the sent data object is correct
+ mock.onPut('/todos').replyOnce((req) => {
+ expect(req.data).toContain('text');
+ expect(req.data).toContain('title');
- it('should render todos', (done) => {
- setTimeout(() => {
- expect(vm.$el.querySelectorAll('.js-todo-list div').length).toBe(1);
- done();
- }, 0);
+ return [201, {}];
});
- });
- describe('add todo', () => {
- let vm;
- beforeEach(() => {
- const Component = Vue.extend(component);
- vm = new Component().$mount();
- });
- it('should add a todos', (done) => {
- setTimeout(() => {
- vm.$el.querySelector('.js-add-todo').click();
+ vm.$el.querySelector('.js-add-todo').click();
- // Add a new interceptor to mock the add Todo request
- Vue.nextTick(() => {
- expect(vm.$el.querySelectorAll('.js-todo-list div').length).toBe(2);
- });
- }, 0);
+ // Add a new interceptor to mock the add Todo request
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelectorAll('.js-todo-list div').length).toBe(2);
+ done();
});
});
});
```
-#### `mountComponent` helper
+
+### `mountComponent` helper
There is a helper in `spec/javascripts/helpers/vue_mount_component_helper.js` that allows you to mount a component with the given props:
```javascript
@@ -447,13 +433,10 @@ const data = {prop: 'foo'};
const vm = mountComponent(Component, data);
```
-#### Test the component's output
+### Test the component's output
The main return value of a Vue component is the rendered output. In order to test the component we
need to test the rendered output. [Vue][vue-test] guide's to unit test show us exactly that:
-### Stubbing API responses
-Refer to [mock axios](axios.md#mock-axios-response-on-tests)
-
[vue-docs]: http://vuejs.org/guide/index.html
[issue-boards]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/app/assets/javascripts/boards
@@ -466,4 +449,3 @@ Refer to [mock axios](axios.md#mock-axios-response-on-tests)
[issue-boards-service]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/app/assets/javascripts/boards/services/board_service.js.es6
[flux]: https://facebook.github.io/flux
[axios]: https://github.com/axios/axios
-[axios-interceptors]: https://github.com/axios/axios#interceptors
diff --git a/doc/development/new_fe_guide/development/accessibility.md b/doc/development/new_fe_guide/development/accessibility.md
index ed35f08432f..2a3a126ca5c 100644
--- a/doc/development/new_fe_guide/development/accessibility.md
+++ b/doc/development/new_fe_guide/development/accessibility.md
@@ -1,3 +1,48 @@
-# Accessibility
+# Accessiblity
+Using semantic HTML plays a key role when it comes to accessibility.
-> TODO: Add content
+## Accessible Rich Internet Applications - ARIA
+WAI-ARIA, the Accessible Rich Internet Applications specification, defines a way to make Web content and Web applications more accessible to people with disabilities.
+
+> Note: It is [recommended][using-aria] to use semantic elements as the primary method to achieve accessibility rather than adding aria attributes. Adding aria attributes should be seen as a secondary method for creating accessible elements.
+
+### Role
+The `role` attribute describes the role the element plays in the context of the document.
+
+Check the list of WAI-ARIA roles [here][roles]
+
+## Icons
+When using icons or images that aren't absolutely needed to understand the context, we should use `aria-hidden="true"`.
+
+On the other hand, if an icon is crucial to understand the context we should do one of the following:
+1. Use `aria-label` in the element with a meaningful description
+1. Use `aria-labelledby` to point to an element that contains the explanation for that icon
+
+## Form inputs
+In forms we should use the `for` attribute in the label statement:
+```
+<div>
+ <label for="name">Fill in your name:</label>
+ <input type="text" id="name" name="name">
+</div>
+```
+
+## Testing
+
+1. On MacOS you can use [VoiceOver][voice-over] by pressing `cmd+F5`.
+1. On Windows you can use [Narrator][narrator] by pressing Windows logo key + Ctrl + Enter.
+
+## Online resources
+
+- [Chrome Accessibility Developer Tools][dev-tools] for testing accessibility
+- [Audit Rules Page][audit-rules] for best practices
+- [Lighthouse Accessibility Score][lighthouse] for accessibility audits
+
+[using-aria]: https://www.w3.org/TR/using-aria/#notes2
+[dev-tools]: https://github.com/GoogleChrome/accessibility-developer-tools
+[audit-rules]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules
+[aria-w3c]: https://www.w3.org/TR/wai-aria-1.1/
+[roles]: https://www.w3.org/TR/wai-aria-1.1/#landmark_roles
+[voice-over]: https://www.apple.com/accessibility/mac/vision/
+[narrator]: https://www.microsoft.com/en-us/accessibility/windows
+[lighthouse]: https://developers.google.com/web/tools/lighthouse/scoring#a11y
diff --git a/doc/user/project/integrations/img/kubernetes_configuration.png b/doc/user/project/integrations/img/kubernetes_configuration.png
deleted file mode 100644
index e535e2b8d46..00000000000
--- a/doc/user/project/integrations/img/kubernetes_configuration.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/integrations/kubernetes.md b/doc/user/project/integrations/kubernetes.md
index f502d1c9821..9342a2cbb00 100644
--- a/doc/user/project/integrations/kubernetes.md
+++ b/doc/user/project/integrations/kubernetes.md
@@ -1,137 +1 @@
----
-last_updated: 2017-12-28
----
-
-# GitLab Kubernetes / OpenShift integration
-
-CAUTION: **Warning:**
-The Kubernetes service integration has been deprecated in GitLab 10.3. If the
-service is active, the cluster information will still be editable, however we
-advise to disable and reconfigure the clusters using the new
-[Clusters](../clusters/index.md) page. If the service is inactive, the fields
-will not be editable. Read [GitLab 10.3 release post](https://about.gitlab.com/2017/12/22/gitlab-10-3-released/#kubernetes-integration-service) for more information.
-
-GitLab can be configured to interact with Kubernetes, or other systems using the
-Kubernetes API (such as OpenShift).
-
-Each project can be configured to connect to a different Kubernetes cluster, see
-the [configuration](#configuration) section.
-
-## Configuration
-
-Navigate to the [Integrations page](project_services.md#accessing-the-project-services)
-of your project and select the **Kubernetes** service to configure it. Fill in
-all the needed parameters, check the "Active" checkbox and hit **Save changes**
-for the changes to take effect.
-
-![Kubernetes configuration settings](img/kubernetes_configuration.png)
-
-The Kubernetes service takes the following parameters:
-
-- **API URL** -
- It's the URL that GitLab uses to access the Kubernetes API. Kubernetes
- exposes several APIs, we want the "base" URL that is common to all of them,
- e.g., `https://kubernetes.example.com` rather than `https://kubernetes.example.com/api/v1`.
-- **CA certificate** (optional) -
- If the API is using a self-signed TLS certificate, you'll also need to include
- the `ca.crt` contents here.
-- **Project namespace** (optional) - The following apply:
- - By default you don't have to fill it in; by leaving it blank, GitLab will
- create one for you.
- - Each project should have a unique namespace.
- - The project namespace is not necessarily the namespace of the secret, if
- you're using a secret with broader permissions, like the secret from `default`.
- - You should **not** use `default` as the project namespace.
- - If you or someone created a secret specifically for the project, usually
- with limited permissions, the secret's namespace and project namespace may
- be the same.
-- **Token** -
- GitLab authenticates against Kubernetes using service tokens, which are
- scoped to a particular `namespace`. If you don't have a service token yet,
- you can follow the
- [Kubernetes documentation](https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/)
- to create one. You can also view or create service tokens in the
- [Kubernetes dashboard](https://kubernetes.io/docs/tasks/access-application-cluster/web-ui-dashboard/#config)
- (under **Config > Secrets**).
-
-TIP: **Tip:**
-If you have a single cluster that you want to use for all your projects,
-you can pre-fill the settings page with a default template. To configure the
-template, see [Services Templates](services_templates.md).
-
-## Deployment variables
-
-The Kubernetes service exposes the following
-[deployment variables](../../../ci/variables/README.md#deployment-variables) in the
-GitLab CI/CD build environment:
-
-- `KUBE_URL` - Equal to the API URL.
-- `KUBE_TOKEN` - The Kubernetes token.
-- `KUBE_NAMESPACE` - The Kubernetes namespace is auto-generated if not specified.
- The default value is `<project_name>-<project_id>`. You can overwrite it to
- use different one if needed, otherwise the `KUBE_NAMESPACE` variable will
- receive the default value.
-- `KUBE_CA_PEM_FILE` - Only present if a custom CA bundle was specified. Path
- to a file containing PEM data.
-- `KUBE_CA_PEM` (deprecated) - Only if a custom CA bundle was specified. Raw PEM data.
-- `KUBECONFIG` - Path to a file containing `kubeconfig` for this deployment.
- CA bundle would be embedded if specified.
-
-## What you can get with the Kubernetes integration
-
-Here's what you can do with GitLab if you enable the Kubernetes integration.
-
-### Deploy Boards
-
-> Available in [GitLab Premium][ee].
-
-GitLab's Deploy Boards offer a consolidated view of the current health and
-status of each CI [environment](../../../ci/environments.md) running on Kubernetes,
-displaying the status of the pods in the deployment. Developers and other
-teammates can view the progress and status of a rollout, pod by pod, in the
-workflow they already use without any need to access Kubernetes.
-
-[> Read more about Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html)
-
-### Canary Deployments
-
-> Available in [GitLab Premium][ee].
-
-Leverage [Kubernetes' Canary deployments](https://kubernetes.io/docs/concepts/cluster-administration/manage-deployment/#canary-deployments)
-and visualize your canary deployments right inside the Deploy Board, without
-the need to leave GitLab.
-
-[> Read more about Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html)
-
-### Kubernetes monitoring
-
-Automatically detect and monitor Kubernetes metrics. Automatic monitoring of
-[NGINX ingress](./prometheus_library/nginx.md) is also supported.
-
-[> Read more about Kubernetes monitoring](prometheus_library/kubernetes.md)
-
-### Auto DevOps
-
-Auto DevOps automatically detects, builds, tests, deploys, and monitors your
-applications.
-
-To make full use of Auto DevOps(Auto Deploy, Auto Review Apps, and Auto Monitoring)
-you will need the Kubernetes project integration enabled.
-
-[> Read more about Auto DevOps](../../../topics/autodevops/index.md)
-
-### Web terminals
-
-NOTE: **Note:**
-Introduced in GitLab 8.15. You must be the project owner or have `master` permissions
-to use terminals. Support is limited to the first container in the
-first pod of your environment.
-
-When enabled, the Kubernetes service adds [web terminal](../../../ci/environments.md#web-terminals)
-support to your [environments](../../../ci/environments.md). This is based on the `exec` functionality found in
-Docker and Kubernetes, so you get a new shell session within your existing
-containers. To use this integration, you should deploy to Kubernetes using
-the deployment variables above, ensuring any pods you create are labelled with
-`app=$CI_ENVIRONMENT_SLUG`. GitLab will do the rest!
-
-[ee]: https://about.gitlab.com/products/
+This document was moved to [another location](../clusters/index.md).
diff --git a/doc/user/project/integrations/project_services.md b/doc/user/project/integrations/project_services.md
index 074eeb729e3..8c51eb9915e 100644
--- a/doc/user/project/integrations/project_services.md
+++ b/doc/user/project/integrations/project_services.md
@@ -39,7 +39,6 @@ Click on the service links to see further configuration instructions and details
| [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway |
| [JIRA](jira.md) | JIRA issue tracker |
| JetBrains TeamCity CI | A continuous integration and build server |
-| [Kubernetes](kubernetes.md) _(Has been deprecated in GitLab 10.3)_ | A containerized deployment service |
| [Mattermost slash commands](mattermost_slash_commands.md) | Mattermost chat and ChatOps slash commands |
| [Mattermost Notifications](mattermost.md) | Receive event notifications in Mattermost |
| [Microsoft teams](microsoft_teams.md) | Receive notifications for actions that happen on GitLab into a room on Microsoft Teams using Office 365 Connectors |
diff --git a/lib/api/api.rb b/lib/api/api.rb
index de20b2b8e67..206fabe5c43 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -15,7 +15,8 @@ module API
include: [
GrapeLogging::Loggers::FilterParameters.new,
GrapeLogging::Loggers::ClientEnv.new,
- Gitlab::GrapeLogging::Loggers::UserLogger.new
+ Gitlab::GrapeLogging::Loggers::UserLogger.new,
+ Gitlab::GrapeLogging::Loggers::QueueDurationLogger.new
]
allow_access_with_scope :api
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 174c5af91d5..3e615f7ac05 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -1020,6 +1020,7 @@ module API
class Job < JobBasic
expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? }
expose :runner, with: Runner
+ expose :artifacts_expire_at
end
class JobBasicWithProject < JobBasic
diff --git a/lib/api/helpers/internal_helpers.rb b/lib/api/helpers/internal_helpers.rb
index abe3d353984..83151be82ad 100644
--- a/lib/api/helpers/internal_helpers.rb
+++ b/lib/api/helpers/internal_helpers.rb
@@ -89,12 +89,6 @@ module API
end
end
- # Return the repository full path so that gitlab-shell has it when
- # handling ssh commands
- def repository_path
- repository.path_to_repo
- end
-
# Return the Gitaly Address if it is enabled
def gitaly_payload(action)
return unless %w[git-receive-pack git-upload-pack git-upload-archive].include?(action)
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index a3dac36b8b6..a9803be9f69 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -59,7 +59,11 @@ module API
status: true,
gl_repository: gl_repository,
gl_username: user&.username,
- repository_path: repository_path,
+
+ # This repository_path is a bogus value but gitlab-shell still requires
+ # its presence. https://gitlab.com/gitlab-org/gitlab-shell/issues/135
+ repository_path: '/',
+
gitaly: gitaly_payload(params[:action])
}
end
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index a7f1cb1131f..5b7ae89440c 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -123,6 +123,7 @@ module API
end
put '/:id' do
job = authenticate_job!
+ forbidden!('Job is not running') unless job.running?
job.trace.set(params[:trace]) if params[:trace]
@@ -131,9 +132,9 @@ module API
case params[:state].to_s
when 'success'
- job.success
+ job.success!
when 'failed'
- job.drop(params[:failure_reason] || :unknown_failure)
+ job.drop!(params[:failure_reason] || :unknown_failure)
end
end
diff --git a/lib/api/settings.rb b/lib/api/settings.rb
index e31c332b6e4..d727ad59367 100644
--- a/lib/api/settings.rb
+++ b/lib/api/settings.rb
@@ -24,7 +24,7 @@ module API
optional :default_project_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default project visibility'
optional :default_snippet_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default snippet visibility'
optional :default_group_visibility, type: String, values: Gitlab::VisibilityLevel.string_values, desc: 'The default group visibility'
- optional :restricted_visibility_levels, type: Array[String], desc: 'Selected levels cannot be used by non-admin users for projects or snippets. If the public level is restricted, user profiles are only visible to logged in users.'
+ optional :restricted_visibility_levels, type: Array[String], desc: '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.'
optional :import_sources, type: Array[String], values: %w[github bitbucket gitlab google_code fogbugz git gitlab_project],
desc: 'Enabled sources for code import during project creation. OmniAuth must be configured for GitHub, Bitbucket, and GitLab.com'
optional :disabled_oauth_sign_in_sources, type: Array[String], desc: 'Disable certain OAuth sign-in sources'
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index c3360c391af..84670d6582e 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -73,29 +73,40 @@ module Backup
end
def prepare_directories
- # TODO: Need to find a way to do this for gitaly
- # Gitaly discussion issue: https://gitlab.com/gitlab-org/gitaly/issues/1194
-
Gitlab.config.repositories.storages.each do |name, repository_storage|
- path = repository_storage.legacy_disk_path
- next unless File.exist?(path)
-
- # Move all files in the existing repos directory except . and .. to
- # repositories.old.<timestamp> directory
- bk_repos_path = File.join(Gitlab.config.backup.path, "tmp", "#{name}-repositories.old." + Time.now.to_i.to_s)
- FileUtils.mkdir_p(bk_repos_path, mode: 0700)
- files = Dir.glob(File.join(path, "*"), File::FNM_DOTMATCH) - [File.join(path, "."), File.join(path, "..")]
-
- begin
- FileUtils.mv(files, bk_repos_path)
- rescue Errno::EACCES
- access_denied_error(path)
- rescue Errno::EBUSY
- resource_busy_error(path)
+ delete_all_repositories(name, repository_storage)
+ end
+ end
+
+ def delete_all_repositories(name, repository_storage)
+ gitaly_migrate(:delete_all_repositories) do |is_enabled|
+ if is_enabled
+ Gitlab::GitalyClient::StorageService.new(name).delete_all_repositories
+ else
+ local_delete_all_repositories(name, repository_storage)
end
end
end
+ def local_delete_all_repositories(name, repository_storage)
+ path = repository_storage.legacy_disk_path
+ return unless File.exist?(path)
+
+ # Move all files in the existing repos directory except . and .. to
+ # repositories.old.<timestamp> directory
+ bk_repos_path = File.join(Gitlab.config.backup.path, "tmp", "#{name}-repositories.old." + Time.now.to_i.to_s)
+ FileUtils.mkdir_p(bk_repos_path, mode: 0700)
+ files = Dir.glob(File.join(path, "*"), File::FNM_DOTMATCH) - [File.join(path, "."), File.join(path, "..")]
+
+ begin
+ FileUtils.mv(files, bk_repos_path)
+ rescue Errno::EACCES
+ access_denied_error(path)
+ rescue Errno::EBUSY
+ resource_busy_error(path)
+ end
+ end
+
def restore_custom_hooks(project)
# TODO: Need to find a way to do this for gitaly
# Gitaly migration issue: https://gitlab.com/gitlab-org/gitaly/issues/1195
@@ -113,6 +124,7 @@ module Backup
def restore
prepare_directories
gitlab_shell = Gitlab::Shell.new
+
Project.find_each(batch_size: 1000) do |project|
progress.print " * #{project.full_path} ... "
path_to_project_bundle = path_to_bundle(project)
@@ -121,7 +133,6 @@ module Backup
restore_repo_success = nil
if File.exist?(path_to_project_bundle)
begin
- gitlab_shell.remove_repository(project.repository_storage, project.disk_path) if project.repository_exists?
project.repository.create_from_bundle path_to_project_bundle
restore_repo_success = true
rescue => e
@@ -146,7 +157,6 @@ module Backup
if File.exist?(path_to_wiki_bundle)
progress.print " * #{wiki.full_path} ... "
begin
- gitlab_shell.remove_repository(wiki.repository_storage, wiki.disk_path) if wiki.repository_exists?
wiki.repository.create_from_bundle(path_to_wiki_bundle)
progress.puts "[DONE]".color(:green)
rescue => e
@@ -224,5 +234,11 @@ module Backup
def display_repo_path(project)
project.hashed_storage?(:repository) ? "#{project.full_path} (#{project.disk_path})" : project.full_path
end
+
+ def gitaly_migrate(method, status: Gitlab::GitalyClient::MigrationStatus::OPT_IN, &block)
+ Gitlab::GitalyClient.migrate(method, status: status, &block)
+ rescue GRPC::NotFound, GRPC::BadStatus => e
+ raise Error, e
+ end
end
end
diff --git a/lib/banzai/reference_parser/issue_parser.rb b/lib/banzai/reference_parser/issue_parser.rb
index 6bee5ea15b9..7b5915899cf 100644
--- a/lib/banzai/reference_parser/issue_parser.rb
+++ b/lib/banzai/reference_parser/issue_parser.rb
@@ -69,7 +69,8 @@ module Banzai
{ group: [:owners, :group_members] },
:invited_groups,
:project_members,
- :project_feature
+ :project_feature,
+ :route
]
}
),
diff --git a/lib/gitlab/auth/user_access_denied_reason.rb b/lib/gitlab/auth/user_access_denied_reason.rb
index af310aa12fc..1893cb001b2 100644
--- a/lib/gitlab/auth/user_access_denied_reason.rb
+++ b/lib/gitlab/auth/user_access_denied_reason.rb
@@ -8,12 +8,12 @@ module Gitlab
def rejection_message
case rejection_type
when :internal
- 'This action cannot be performed by internal users'
+ "This action cannot be performed by internal users"
when :terms_not_accepted
- 'You must accept the Terms of Service in order to perform this action. '\
- 'Please access GitLab from a web browser to accept these terms.'
+ "You (#{@user.to_reference}) must accept the Terms of Service in order to perform this action. "\
+ "Please access GitLab from a web browser to accept these terms."
else
- 'Your account has been blocked.'
+ "Your account has been blocked."
end
end
diff --git a/lib/gitlab/background_migration/fill_file_store_job_artifact.rb b/lib/gitlab/background_migration/fill_file_store_job_artifact.rb
new file mode 100644
index 00000000000..22b0ac71920
--- /dev/null
+++ b/lib/gitlab/background_migration/fill_file_store_job_artifact.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+# rubocop:disable Metrics/AbcSize
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class FillFileStoreJobArtifact
+ class JobArtifact < ActiveRecord::Base
+ self.table_name = 'ci_job_artifacts'
+ end
+
+ def perform(start_id, stop_id)
+ FillFileStoreJobArtifact::JobArtifact
+ .where(file_store: nil)
+ .where(id: (start_id..stop_id))
+ .update_all(file_store: 1)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/fill_file_store_lfs_object.rb b/lib/gitlab/background_migration/fill_file_store_lfs_object.rb
new file mode 100644
index 00000000000..d0816ae3ed5
--- /dev/null
+++ b/lib/gitlab/background_migration/fill_file_store_lfs_object.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+# rubocop:disable Metrics/AbcSize
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class FillFileStoreLfsObject
+ class LfsObject < ActiveRecord::Base
+ self.table_name = 'lfs_objects'
+ end
+
+ def perform(start_id, stop_id)
+ FillFileStoreLfsObject::LfsObject
+ .where(file_store: nil)
+ .where(id: (start_id..stop_id))
+ .update_all(file_store: 1)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/fill_store_upload.rb b/lib/gitlab/background_migration/fill_store_upload.rb
new file mode 100644
index 00000000000..94c65459a67
--- /dev/null
+++ b/lib/gitlab/background_migration/fill_store_upload.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+# rubocop:disable Metrics/AbcSize
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class FillStoreUpload
+ class Upload < ActiveRecord::Base
+ self.table_name = 'uploads'
+ self.inheritance_column = :_type_disabled
+ end
+
+ def perform(start_id, stop_id)
+ FillStoreUpload::Upload
+ .where(store: nil)
+ .where(id: (start_id..stop_id))
+ .update_all(store: 1)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/count.rb b/lib/gitlab/database/count.rb
index 3374203960e..5f549ed2b3c 100644
--- a/lib/gitlab/database/count.rb
+++ b/lib/gitlab/database/count.rb
@@ -17,31 +17,69 @@ module Gitlab
].freeze
end
- def self.approximate_count(model)
- return model.count unless Gitlab::Database.postgresql?
+ # Takes in an array of models and returns a Hash for the approximate
+ # counts for them. If the model's table has not been vacuumed or
+ # analyzed recently, simply run the Model.count to get the data.
+ #
+ # @param [Array]
+ # @return [Hash] of Model -> count mapping
+ def self.approximate_counts(models)
+ table_to_model_map = models.each_with_object({}) do |model, hash|
+ hash[model.table_name] = model
+ end
- execute_estimate_if_updated_recently(model) || model.count
- end
+ table_names = table_to_model_map.keys
+ counts_by_table_name = Gitlab::Database.postgresql? ? reltuples_from_recently_updated(table_names) : {}
- def self.execute_estimate_if_updated_recently(model)
- ActiveRecord::Base.connection.select_value(postgresql_estimate_query(model)).to_i if reltuples_updated_recently?(model)
- rescue *CONNECTION_ERRORS
+ # Convert table -> count to Model -> count
+ counts_by_model = counts_by_table_name.each_with_object({}) do |pair, hash|
+ model = table_to_model_map[pair.first]
+ hash[model] = pair.second
+ end
+
+ missing_tables = table_names - counts_by_table_name.keys
+
+ missing_tables.each do |table|
+ model = table_to_model_map[table]
+ counts_by_model[model] = model.count
+ end
+
+ counts_by_model
end
- def self.reltuples_updated_recently?(model)
- time = "to_timestamp(#{1.hour.ago.to_i})"
- query = <<~SQL
- SELECT 1 FROM pg_stat_user_tables WHERE relname = '#{model.table_name}' AND
- (last_vacuum > #{time} OR last_autovacuum > #{time} OR last_analyze > #{time} OR last_autoanalyze > #{time})
- SQL
+ # Returns a hash of the table names that have recently updated tuples.
+ #
+ # @param [Array] table names
+ # @returns [Hash] Table name to count mapping (e.g. { 'projects' => 5, 'users' => 100 })
+ def self.reltuples_from_recently_updated(table_names)
+ query = postgresql_estimate_query(table_names)
+ rows = []
- ActiveRecord::Base.connection.select_all(query).count > 0
+ # Querying tuple stats only works on the primary. Due to load
+ # balancing, we need to ensure this query hits the load balancer. The
+ # easiest way to do this is to start a transaction.
+ ActiveRecord::Base.transaction do
+ rows = ActiveRecord::Base.connection.select_all(query)
+ end
+
+ rows.each_with_object({}) { |row, data| data[row['table_name']] = row['estimate'].to_i }
rescue *CONNECTION_ERRORS
- false
+ {}
end
- def self.postgresql_estimate_query(model)
- "SELECT reltuples::bigint AS estimate FROM pg_class where relname = '#{model.table_name}'"
+ # Generates the PostgreSQL query to return the tuples for tables
+ # that have been vacuumed or analyzed in the last hour.
+ #
+ # @param [Array] table names
+ # @returns [Hash] Table name to count mapping (e.g. { 'projects' => 5, 'users' => 100 })
+ def self.postgresql_estimate_query(table_names)
+ time = "to_timestamp(#{1.hour.ago.to_i})"
+ <<~SQL
+ SELECT pg_class.relname AS table_name, reltuples::bigint AS estimate FROM pg_class
+ LEFT JOIN pg_stat_user_tables ON pg_class.relname = pg_stat_user_tables.relname
+ WHERE pg_class.relname IN (#{table_names.map { |table| "'#{table}'" }.join(',')})
+ AND (last_vacuum > #{time} OR last_autovacuum > #{time} OR last_analyze > #{time} OR last_autoanalyze > #{time})
+ SQL
end
end
end
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index 014854da55c..765fb0289a8 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -76,6 +76,13 @@ module Gitlab
line_code(line) if line
end
+ # Returns the raw diff content up to the given line index
+ def diff_hunk(diff_line)
+ # Adding 2 because of the @@ diff header and Enum#take should consider
+ # an extra line, because we're passing an index.
+ raw_diff.each_line.take(diff_line.index + 2).join
+ end
+
def old_sha
diff_refs&.base_sha
end
diff --git a/lib/gitlab/gitaly_client/storage_service.rb b/lib/gitlab/gitaly_client/storage_service.rb
new file mode 100644
index 00000000000..eb0e910665b
--- /dev/null
+++ b/lib/gitlab/gitaly_client/storage_service.rb
@@ -0,0 +1,15 @@
+module Gitlab
+ module GitalyClient
+ class StorageService
+ def initialize(storage)
+ @storage = storage
+ end
+
+ # Delete all repositories in the storage. This is a slow and VERY DESTRUCTIVE operation.
+ def delete_all_repositories
+ request = Gitaly::DeleteAllRepositoriesRequest.new(storage_name: @storage)
+ GitalyClient.call(@storage, :storage_service, :delete_all_repositories, request)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/grape_logging/loggers/queue_duration_logger.rb b/lib/gitlab/grape_logging/loggers/queue_duration_logger.rb
new file mode 100644
index 00000000000..0adac79f25a
--- /dev/null
+++ b/lib/gitlab/grape_logging/loggers/queue_duration_logger.rb
@@ -0,0 +1,26 @@
+# This grape_logging module (https://github.com/aserafin/grape_logging) makes it
+# possible to log how much time an API request was queued by Workhorse.
+module Gitlab
+ module GrapeLogging
+ module Loggers
+ class QueueDurationLogger < ::GrapeLogging::Loggers::Base
+ attr_accessor :start_time
+
+ def before
+ @start_time = Time.now
+ end
+
+ def parameters(request, _)
+ proxy_start = request.env['HTTP_GITLAB_WORKHORSE_PROXY_START'].presence
+
+ return {} unless proxy_start && start_time
+
+ # Time in milliseconds since gitlab-workhorse started the request
+ duration = (start_time.to_f * 1_000 - proxy_start.to_f / 1_000_000).round(2)
+
+ { 'queue_duration': duration }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/google_api/cloud_platform/client.rb b/lib/google_api/cloud_platform/client.rb
index f30dd995695..36859b4d025 100644
--- a/lib/google_api/cloud_platform/client.rb
+++ b/lib/google_api/cloud_platform/client.rb
@@ -1,3 +1,4 @@
+require 'google/apis/compute_v1'
require 'google/apis/container_v1'
require 'google/apis/cloudbilling_v1'
require 'google/apis/cloudresourcemanager_v1'
@@ -42,22 +43,6 @@ module GoogleApi
true
end
- def projects_list
- service = Google::Apis::CloudresourcemanagerV1::CloudResourceManagerService.new
- service.authorization = access_token
-
- service.fetch_all(items: :projects) do |token|
- service.list_projects(page_token: token, options: user_agent_header)
- end
- end
-
- def projects_get_billing_info(project_id)
- service = Google::Apis::CloudbillingV1::CloudbillingService.new
- service.authorization = access_token
-
- service.get_project_billing_info("projects/#{project_id}", options: user_agent_header)
- end
-
def projects_zones_clusters_get(project_id, zone, cluster_id)
service = Google::Apis::ContainerV1::ContainerService.new
service.authorization = access_token
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 608d2a584ba..9e34eb463ce 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-05-21 12:38-0700\n"
-"PO-Revision-Date: 2018-05-21 12:38-0700\n"
+"POT-Creation-Date: 2018-05-23 07:40-0500\n"
+"PO-Revision-Date: 2018-05-23 07:40-0500\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -3517,7 +3517,7 @@ msgstr ""
msgid "Seconds to wait for a storage access attempt"
msgstr ""
-msgid "Secret variables"
+msgid "Variables"
msgstr ""
msgid "Select Archive Format"
@@ -3998,6 +3998,12 @@ msgstr ""
msgid "There are problems accessing Git storage: "
msgstr ""
+msgid "There was an error loading jobs"
+msgstr ""
+
+msgid "There was an error loading latest pipeline"
+msgstr ""
+
msgid "There was an error loading users activity calendar."
msgstr ""
@@ -4241,10 +4247,10 @@ msgstr ""
msgid "Timeago|in 1 year"
msgstr ""
-msgid "Timeago|in a while"
+msgid "Timeago|less than a minute ago"
msgstr ""
-msgid "Timeago|less than a minute ago"
+msgid "Timeago|right now"
msgstr ""
msgid "Time|hr"
@@ -4479,7 +4485,31 @@ msgstr ""
msgid "WikiEdit|There is already a page with the same title in that path."
msgstr ""
-msgid "WikiEmptyPageError|You are not allowed to create wiki pages"
+msgid "WikiEmptyIssueMessage|Suggest wiki improvement"
+msgstr ""
+
+msgid "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}."
+msgstr ""
+
+msgid "WikiEmptyIssueMessage|issue tracker"
+msgstr ""
+
+msgid "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."
+msgstr ""
+
+msgid "WikiEmpty|Create your first page"
+msgstr ""
+
+msgid "WikiEmpty|Suggest wiki improvement"
+msgstr ""
+
+msgid "WikiEmpty|The wiki lets you write documentation for your project"
+msgstr ""
+
+msgid "WikiEmpty|This project has no wiki pages"
+msgstr ""
+
+msgid "WikiEmpty|You must be a project member in order to add wiki pages."
msgstr ""
msgid "WikiHistoricalPage|This is an old version of this page."
@@ -4548,9 +4578,6 @@ msgstr ""
msgid "Wiki|Edit Page"
msgstr ""
-msgid "Wiki|Empty page"
-msgstr ""
-
msgid "Wiki|More Pages"
msgstr ""
diff --git a/package.json b/package.json
index 13be495b702..defcbd34d95 100644
--- a/package.json
+++ b/package.json
@@ -25,7 +25,7 @@
"babel-preset-latest": "^6.24.1",
"babel-preset-stage-2": "^6.24.1",
"blackst0ne-mermaid": "^7.1.0-fixed",
- "bootstrap": "4.1",
+ "bootstrap": "~4.1.1",
"brace-expansion": "^1.1.8",
"cache-loader": "^1.2.2",
"chart.js": "1.0.2",
diff --git a/qa/qa/page/project/settings/ci_cd.rb b/qa/qa/page/project/settings/ci_cd.rb
index 17a1bc904e4..145c3d3ddfa 100644
--- a/qa/qa/page/project/settings/ci_cd.rb
+++ b/qa/qa/page/project/settings/ci_cd.rb
@@ -7,7 +7,7 @@ module QA # rubocop:disable Naming/FileName
view 'app/views/projects/settings/ci_cd/show.html.haml' do
element :runners_settings, 'Runners settings'
- element :secret_variables, 'Secret variables'
+ element :secret_variables, 'Variables'
end
def expand_runners_settings(&block)
@@ -17,7 +17,7 @@ module QA # rubocop:disable Naming/FileName
end
def expand_secret_variables(&block)
- expand_section('Secret variables') do
+ expand_section('Variables') do
Settings::SecretVariables.perform(&block)
end
end
diff --git a/qa/qa/specs/features/api/users_spec.rb b/qa/qa/specs/features/api/users_spec.rb
index d4ff4ebbc9a..38f4c497183 100644
--- a/qa/qa/specs/features/api/users_spec.rb
+++ b/qa/qa/specs/features/api/users_spec.rb
@@ -17,17 +17,16 @@ module QA
get request.url, { params: { username: Runtime::User.name } }
expect_status(200)
- expect(json_body).to be_an Array
- expect(json_body.size).to eq(1)
- expect(json_body.first[:username]).to eq Runtime::User.name
+ expect(json_body).to contain_exactly(
+ a_hash_including(username: Runtime::User.name)
+ )
end
scenario 'submit request with an invalid user name' do
get request.url, { params: { username: SecureRandom.hex(10) } }
expect_status(200)
- expect(json_body).to be_an Array
- expect(json_body.size).to eq(0)
+ expect(json_body).to eq([])
end
end
diff --git a/spec/controllers/projects/clusters/gcp_controller_spec.rb b/spec/controllers/projects/clusters/gcp_controller_spec.rb
index 715bb9f5e52..271ba37aed4 100644
--- a/spec/controllers/projects/clusters/gcp_controller_spec.rb
+++ b/spec/controllers/projects/clusters/gcp_controller_spec.rb
@@ -77,8 +77,6 @@ describe Projects::Clusters::GcpController do
end
it 'has new object' do
- expect(controller).to receive(:authorize_google_project_billing)
-
go
expect(assigns(:cluster)).to be_an_instance_of(Clusters::Cluster)
@@ -137,33 +135,15 @@ describe Projects::Clusters::GcpController do
context 'when access token is valid' do
before do
stub_google_api_validate_token
- allow_any_instance_of(described_class).to receive(:authorize_google_project_billing)
- end
-
- context 'when google project billing is enabled' do
- before do
- redis_double = double.as_null_object
- allow(Gitlab::Redis::SharedState).to receive(:with).and_yield(redis_double)
- allow(redis_double).to receive(:get).with(CheckGcpProjectBillingWorker.redis_shared_state_key_for('token')).and_return('true')
- end
-
- it 'creates a new cluster' do
- expect(ClusterProvisionWorker).to receive(:perform_async)
- expect { go }.to change { Clusters::Cluster.count }
- .and change { Clusters::Providers::Gcp.count }
- expect(response).to redirect_to(project_cluster_path(project, project.clusters.first))
- expect(project.clusters.first).to be_gcp
- expect(project.clusters.first).to be_kubernetes
- end
end
- context 'when google project billing is not enabled' do
- it 'renders the cluster form with an error' do
- go
-
- expect(response).to set_flash.now[:alert]
- expect(response).to render_template('new')
- end
+ it 'creates a new cluster' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
+ expect { go }.to change { Clusters::Cluster.count }
+ .and change { Clusters::Providers::Gcp.count }
+ expect(response).to redirect_to(project_cluster_path(project, project.clusters.first))
+ expect(project.clusters.first).to be_gcp
+ expect(project.clusters.first).to be_kubernetes
end
end
diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb
index 1ce30015e81..bf329b0bb94 100644
--- a/spec/features/groups/group_settings_spec.rb
+++ b/spec/features/groups/group_settings_spec.rb
@@ -83,7 +83,7 @@ feature 'Edit group settings' do
attach_file(:group_avatar, Rails.root.join('spec', 'fixtures', 'banana_sample.gif'))
- expect { click_button 'Save group' }.to change { group.reload.avatar? }.to(true)
+ expect { save_group }.to change { group.reload.avatar? }.to(true)
end
it 'uploads new group avatar' do
@@ -97,10 +97,19 @@ feature 'Edit group settings' do
expect(page).not_to have_link('Remove avatar')
end
end
-end
-def update_path(new_group_path)
- visit edit_group_path(group)
- fill_in 'group_path', with: new_group_path
- click_button 'Save group'
+ def update_path(new_group_path)
+ visit edit_group_path(group)
+
+ page.within('.gs-advanced') do
+ fill_in 'group_path', with: new_group_path
+ click_button 'Change group path'
+ end
+ end
+
+ def save_group
+ page.within('.gs-general') do
+ click_button 'Save group'
+ end
+ end
end
diff --git a/spec/features/groups/share_lock_spec.rb b/spec/features/groups/share_lock_spec.rb
index 8842d1391aa..cefbc15e068 100644
--- a/spec/features/groups/share_lock_spec.rb
+++ b/spec/features/groups/share_lock_spec.rb
@@ -15,9 +15,8 @@ feature 'Group share with group lock' do
context 'when enabling the parent group share with group lock' do
scenario 'the subgroup share with group lock becomes enabled' do
visit edit_group_path(root_group)
- check 'group_share_with_group_lock'
- click_on 'Save group'
+ enable_group_lock
expect(subgroup.reload.share_with_group_lock?).to be_truthy
end
@@ -26,16 +25,15 @@ feature 'Group share with group lock' do
context 'when disabling the parent group share with group lock (which was already enabled)' do
background do
visit edit_group_path(root_group)
- check 'group_share_with_group_lock'
- click_on 'Save group'
+
+ enable_group_lock
end
context 'and the subgroup share with group lock is enabled' do
scenario 'the subgroup share with group lock does not change' do
visit edit_group_path(root_group)
- uncheck 'group_share_with_group_lock'
- click_on 'Save group'
+ disable_group_lock
expect(subgroup.reload.share_with_group_lock?).to be_truthy
end
@@ -44,19 +42,32 @@ feature 'Group share with group lock' do
context 'but the subgroup share with group lock is disabled' do
background do
visit edit_group_path(subgroup)
- uncheck 'group_share_with_group_lock'
- click_on 'Save group'
+
+ disable_group_lock
end
scenario 'the subgroup share with group lock does not change' do
visit edit_group_path(root_group)
- uncheck 'group_share_with_group_lock'
- click_on 'Save group'
+ disable_group_lock
expect(subgroup.reload.share_with_group_lock?).to be_falsey
end
end
end
end
+
+ def enable_group_lock
+ page.within('.gs-permissions') do
+ check 'group_share_with_group_lock'
+ click_on 'Save group'
+ end
+ end
+
+ def disable_group_lock
+ page.within('.gs-permissions') do
+ uncheck 'group_share_with_group_lock'
+ click_on 'Save group'
+ end
+ end
end
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index c1f3d94bc20..236768b5d7f 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -141,8 +141,10 @@ feature 'Group' do
end
it 'saves new settings' do
- fill_in 'group_name', with: new_name
- click_button 'Save group'
+ page.within('.gs-general') do
+ fill_in 'group_name', with: new_name
+ click_button 'Save group'
+ end
expect(page).to have_content 'successfully updated'
expect(find('#group_name').value).to eq(new_name)
diff --git a/spec/features/projects/clusters/gcp_spec.rb b/spec/features/projects/clusters/gcp_spec.rb
index a8a627d8806..c85b82b2090 100644
--- a/spec/features/projects/clusters/gcp_spec.rb
+++ b/spec/features/projects/clusters/gcp_spec.rb
@@ -22,152 +22,123 @@ feature 'Gcp Cluster', :js do
.to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
end
- context 'when user has a GCP project with billing enabled' do
+ context 'when user does not have a cluster and visits cluster index page' do
before do
- allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing)
- allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return(true)
- end
-
- context 'when user does not have a cluster and visits cluster index page' do
- before do
- visit project_clusters_path(project)
-
- click_link 'Add Kubernetes cluster'
- click_link 'Create on Google Kubernetes Engine'
- end
-
- context 'when user filled form with valid parameters' do
- before do
- allow_any_instance_of(GoogleApi::CloudPlatform::Client)
- .to receive(:projects_zones_clusters_create) do
- OpenStruct.new(
- self_link: 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123',
- status: 'RUNNING'
- )
- end
-
- allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil)
-
- fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
- fill_in 'cluster_name', with: 'dev-cluster'
- click_button 'Create Kubernetes cluster'
- end
+ visit project_clusters_path(project)
- it 'user sees a cluster details page and creation status' do
- expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...')
+ click_link 'Add Kubernetes cluster'
+ click_link 'Create on Google Kubernetes Engine'
+ end
- Clusters::Cluster.last.provider.make_created!
+ context 'when user filled form with valid parameters' do
+ subject { click_button 'Create Kubernetes cluster' }
- expect(page).to have_content('Kubernetes cluster was successfully created on Google Kubernetes Engine')
+ before do
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_clusters_create) do
+ OpenStruct.new(
+ self_link: 'projects/gcp-project-12345/zones/us-central1-a/operations/ope-123',
+ status: 'RUNNING'
+ )
end
- it 'user sees a error if something worng during creation' do
- expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...')
+ allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil)
- Clusters::Cluster.last.provider.make_errored!('Something wrong!')
+ execute_script('document.querySelector(".js-gke-cluster-creation-submit").removeAttribute("disabled")')
+ sleep 2 # wait for ajax
+ execute_script('document.querySelector(".js-gcp-project-id-dropdown input").setAttribute("type", "text")')
+ execute_script('document.querySelector(".js-gcp-zone-dropdown input").setAttribute("type", "text")')
+ execute_script('document.querySelector(".js-gcp-machine-type-dropdown input").setAttribute("type", "text")')
- expect(page).to have_content('Something wrong!')
- end
+ fill_in 'cluster[name]', with: 'dev-cluster'
+ fill_in 'cluster[provider_gcp_attributes][gcp_project_id]', with: 'gcp-project-123'
+ fill_in 'cluster[provider_gcp_attributes][zone]', with: 'us-central1-a'
+ fill_in 'cluster[provider_gcp_attributes][machine_type]', with: 'n1-standard-2'
end
- context 'when user filled form with invalid parameters' do
- before do
- click_button 'Create Kubernetes cluster'
- end
-
- it 'user sees a validation error' do
- expect(page).to have_css('#error_explanation')
- end
+ it 'users sees a form with the GCP token' do
+ expect(page).to have_selector(:css, 'form[data-token="token"]')
end
- end
- context 'when user does have a cluster and visits cluster page' do
- let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+ it 'user sees a cluster details page and creation status' do
+ subject
- before do
- visit project_cluster_path(project, cluster)
- end
+ expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...')
+
+ Clusters::Cluster.last.provider.make_created!
- it 'user sees a cluster details page' do
- expect(page).to have_button('Save changes')
- expect(page.find(:css, '.cluster-name').value).to eq(cluster.name)
+ expect(page).to have_content('Kubernetes cluster was successfully created on Google Kubernetes Engine')
end
- context 'when user disables the cluster' do
- before do
- page.find(:css, '.js-cluster-enable-toggle-area .js-project-feature-toggle').click
- page.within('#cluster-integration') { click_button 'Save changes' }
- end
+ it 'user sees a error if something wrong during creation' do
+ subject
- it 'user sees the successful message' do
- expect(page).to have_content('Kubernetes cluster was successfully updated.')
- end
- end
+ expect(page).to have_content('Kubernetes cluster is being created on Google Kubernetes Engine...')
- context 'when user changes cluster parameters' do
- before do
- fill_in 'cluster_platform_kubernetes_attributes_namespace', with: 'my-namespace'
- page.within('#js-cluster-details') { click_button 'Save changes' }
- end
+ Clusters::Cluster.last.provider.make_errored!('Something wrong!')
- it 'user sees the successful message' do
- expect(page).to have_content('Kubernetes cluster was successfully updated.')
- expect(cluster.reload.platform_kubernetes.namespace).to eq('my-namespace')
- end
+ expect(page).to have_content('Something wrong!')
end
+ end
- context 'when user destroy the cluster' do
- before do
- page.accept_confirm do
- click_link 'Remove integration'
- end
- end
+ context 'when user filled form with invalid parameters' do
+ before do
+ execute_script('document.querySelector(".js-gke-cluster-creation-submit").removeAttribute("disabled")')
+ click_button 'Create Kubernetes cluster'
+ end
- it 'user sees creation form with the successful message' do
- expect(page).to have_content('Kubernetes cluster integration was successfully removed.')
- expect(page).to have_link('Add Kubernetes cluster')
- end
+ it 'user sees a validation error' do
+ expect(page).to have_css('#error_explanation')
end
end
end
- context 'when user does not have a GCP project with billing enabled' do
- before do
- allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing)
- allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return(false)
-
- visit project_clusters_path(project)
-
- click_link 'Add Kubernetes cluster'
- click_link 'Create on Google Kubernetes Engine'
+ context 'when user does have a cluster and visits cluster page' do
+ let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
- fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
- fill_in 'cluster_name', with: 'dev-cluster'
- click_button 'Create Kubernetes cluster'
+ before do
+ visit project_cluster_path(project, cluster)
end
- it 'user sees form with error' do
- expect(page).to have_content('Please enable billing for one of your projects to be able to create a Kubernetes cluster, then try again.')
+ it 'user sees a cluster details page' do
+ expect(page).to have_button('Save changes')
+ expect(page.find(:css, '.cluster-name').value).to eq(cluster.name)
end
- end
- context 'when gcp billing status is not in redis' do
- before do
- allow_any_instance_of(Projects::Clusters::GcpController).to receive(:authorize_google_project_billing)
- allow_any_instance_of(Projects::Clusters::GcpController).to receive(:google_project_billing_status).and_return(nil)
+ context 'when user disables the cluster' do
+ before do
+ page.find(:css, '.js-cluster-enable-toggle-area .js-project-feature-toggle').click
+ page.within('#cluster-integration') { click_button 'Save changes' }
+ end
- visit project_clusters_path(project)
+ it 'user sees the successful message' do
+ expect(page).to have_content('Kubernetes cluster was successfully updated.')
+ end
+ end
- click_link 'Add Kubernetes cluster'
- click_link 'Create on Google Kubernetes Engine'
+ context 'when user changes cluster parameters' do
+ before do
+ fill_in 'cluster_platform_kubernetes_attributes_namespace', with: 'my-namespace'
+ page.within('#js-cluster-details') { click_button 'Save changes' }
+ end
- fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
- fill_in 'cluster_name', with: 'dev-cluster'
- click_button 'Create Kubernetes cluster'
+ it 'user sees the successful message' do
+ expect(page).to have_content('Kubernetes cluster was successfully updated.')
+ expect(cluster.reload.platform_kubernetes.namespace).to eq('my-namespace')
+ end
end
- it 'user sees form with error' do
- expect(page).to have_content('We could not verify that one of your projects on GCP has billing enabled. Please try again.')
+ context 'when user destroy the cluster' do
+ before do
+ page.accept_confirm do
+ click_link 'Remove integration'
+ end
+ end
+
+ it 'user sees creation form with the successful message' do
+ expect(page).to have_content('Kubernetes cluster integration was successfully removed.')
+ expect(page).to have_link('Add Kubernetes cluster')
+ end
end
end
end
diff --git a/spec/features/projects/user_sees_sidebar_spec.rb b/spec/features/projects/user_sees_sidebar_spec.rb
index 8a9255db9e8..ee5734a9bf1 100644
--- a/spec/features/projects/user_sees_sidebar_spec.rb
+++ b/spec/features/projects/user_sees_sidebar_spec.rb
@@ -44,6 +44,18 @@ describe 'Projects > User sees sidebar' do
expect(page).not_to have_content 'Repository'
expect(page).not_to have_content 'CI / CD'
expect(page).not_to have_content 'Merge Requests'
+ expect(page).not_to have_content 'Operations'
+ end
+ end
+
+ it 'shows build tab if builds are public' do
+ project.public_builds = true
+ project.save
+
+ visit project_path(project)
+
+ within('.nav-sidebar') do
+ expect(page).to have_content 'CI / CD'
end
end
diff --git a/spec/features/projects/wiki/markdown_preview_spec.rb b/spec/features/projects/wiki/markdown_preview_spec.rb
index e473739a6aa..bbdd98a7623 100644
--- a/spec/features/projects/wiki/markdown_preview_spec.rb
+++ b/spec/features/projects/wiki/markdown_preview_spec.rb
@@ -19,6 +19,7 @@ feature 'Projects > Wiki > User previews markdown changes', :js do
visit project_path(project)
find('.shortcuts-wiki').click
+ click_link "Create your first page"
end
context "while creating a new wiki page" do
diff --git a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
index 9989e1ffda7..706894f4b32 100644
--- a/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_creates_wiki_page_spec.rb
@@ -8,6 +8,7 @@ describe "User creates wiki page" do
sign_in(user)
visit(project_wikis_path(project))
+ click_link "Create your first page"
end
context "when wiki is empty" do
diff --git a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
index e019e3ce5a5..272dac127dd 100644
--- a/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_updates_wiki_page_spec.rb
@@ -11,6 +11,7 @@ describe 'User updates wiki page' do
context 'when wiki is empty' do
before do
visit(project_wikis_path(project))
+ click_link "Create your first page"
end
context 'in a user namespace' do
diff --git a/spec/features/projects/wiki/user_views_wiki_empty_spec.rb b/spec/features/projects/wiki/user_views_wiki_empty_spec.rb
new file mode 100644
index 00000000000..83ffbb4a94e
--- /dev/null
+++ b/spec/features/projects/wiki/user_views_wiki_empty_spec.rb
@@ -0,0 +1,75 @@
+require 'spec_helper'
+
+describe 'User views empty wiki' do
+ let(:user) { create(:user) }
+
+ shared_examples 'empty wiki and accessible issues' do
+ it 'show "issue tracker" message' do
+ visit(project_wikis_path(project))
+
+ element = page.find('.row.empty-state')
+
+ expect(element).to have_content('This project has no wiki pages')
+ expect(element).to have_link("issue tracker", href: project_issues_path(project))
+ expect(element).to have_link("Suggest wiki improvement", href: new_project_issue_path(project))
+ end
+ end
+
+ shared_examples 'empty wiki and non-accessible issues' do
+ it 'does not show "issue tracker" message' do
+ visit(project_wikis_path(project))
+
+ element = page.find('.row.empty-state')
+
+ expect(element).to have_content('This project has no wiki pages')
+ expect(element).to have_no_link('Suggest wiki improvement')
+ end
+ end
+
+ context 'when user is logged out and issue tracker is public' do
+ let(:project) { create(:project, :public, :wiki_repo) }
+
+ it_behaves_like 'empty wiki and accessible issues'
+ end
+
+ context 'when user is logged in and not a member' do
+ let(:project) { create(:project, :public, :wiki_repo) }
+
+ before do
+ sign_in(user)
+ end
+
+ it_behaves_like 'empty wiki and accessible issues'
+ end
+
+ context 'when issue tracker is private' do
+ let(:project) { create(:project, :public, :wiki_repo, :issues_private) }
+
+ it_behaves_like 'empty wiki and non-accessible issues'
+ end
+
+ context 'when issue tracker is disabled' do
+ let(:project) { create(:project, :public, :wiki_repo, :issues_disabled) }
+
+ it_behaves_like 'empty wiki and non-accessible issues'
+ end
+
+ context 'when user is logged in and a memeber' do
+ let(:project) { create(:project, :public, :wiki_repo) }
+
+ before do
+ sign_in(user)
+ project.add_developer(user)
+ end
+
+ it 'show "create first page" message' do
+ visit(project_wikis_path(project))
+
+ element = page.find('.row.empty-state')
+
+ element.click_link 'Create your first page'
+
+ expect(page).to have_button('Create page')
+ end
+ end
+end
diff --git a/spec/features/projects/wiki/user_views_wiki_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
index 6661714222a..1de7d9a56a8 100644
--- a/spec/features/projects/wiki/user_views_wiki_page_spec.rb
+++ b/spec/features/projects/wiki/user_views_wiki_page_spec.rb
@@ -18,6 +18,7 @@ describe 'User views a wiki page' do
context 'when wiki is empty' do
before do
visit(project_wikis_path(project))
+ click_link "Create your first page"
click_on('New page')
@@ -140,6 +141,7 @@ describe 'User views a wiki page' do
visit(project_path(project))
find('.shortcuts-wiki').click
+ click_link "Create your first page"
expect(page).to have_content('Home · Create Page')
end
diff --git a/spec/features/uploads/user_uploads_avatar_to_group_spec.rb b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb
index e8884bc1a00..c8db82a562f 100644
--- a/spec/features/uploads/user_uploads_avatar_to_group_spec.rb
+++ b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb
@@ -14,7 +14,9 @@ feature 'User uploads avatar to group' do
visible: false
)
- click_button 'Save group'
+ page.within('.gs-general') do
+ click_button 'Save group'
+ end
visit group_path(group)
diff --git a/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown_spec.js b/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown_spec.js
new file mode 100644
index 00000000000..21805ef0b28
--- /dev/null
+++ b/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown_spec.js
@@ -0,0 +1,103 @@
+import Vue from 'vue';
+import GkeMachineTypeDropdown from '~/projects/gke_cluster_dropdowns/components/gke_machine_type_dropdown.vue';
+import { createStore } from '~/projects/gke_cluster_dropdowns/store';
+import {
+ SET_PROJECT,
+ SET_PROJECT_BILLING_STATUS,
+ SET_ZONE,
+ SET_MACHINE_TYPES,
+} from '~/projects/gke_cluster_dropdowns/store/mutation_types';
+import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import {
+ selectedZoneMock,
+ selectedProjectMock,
+ selectedMachineTypeMock,
+ gapiMachineTypesResponseMock,
+} from '../mock_data';
+
+const componentConfig = {
+ fieldId: 'cluster_provider_gcp_attributes_gcp_machine_type',
+ fieldName: 'cluster[provider_gcp_attributes][gcp_machine_type]',
+};
+
+const LABELS = {
+ LOADING: 'Fetching machine types',
+ DISABLED_NO_PROJECT: 'Select project and zone to choose machine type',
+ DISABLED_NO_ZONE: 'Select zone to choose machine type',
+ DEFAULT: 'Select machine type',
+};
+
+const createComponent = (store, props = componentConfig) => {
+ const Component = Vue.extend(GkeMachineTypeDropdown);
+
+ return mountComponentWithStore(Component, {
+ el: null,
+ props,
+ store,
+ });
+};
+
+describe('GkeMachineTypeDropdown', () => {
+ let vm;
+ let store;
+
+ beforeEach(() => {
+ store = createStore();
+ vm = createComponent(store);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('shows various toggle text depending on state', () => {
+ it('returns disabled state toggle text when no project and zone are selected', () => {
+ expect(vm.toggleText).toBe(LABELS.DISABLED_NO_PROJECT);
+ });
+
+ it('returns disabled state toggle text when no zone is selected', () => {
+ vm.$store.commit(SET_PROJECT, selectedProjectMock);
+ vm.$store.commit(SET_PROJECT_BILLING_STATUS, true);
+
+ expect(vm.toggleText).toBe(LABELS.DISABLED_NO_ZONE);
+ });
+
+ it('returns loading toggle text', () => {
+ vm.isLoading = true;
+
+ expect(vm.toggleText).toBe(LABELS.LOADING);
+ });
+
+ it('returns default toggle text', () => {
+ expect(vm.toggleText).toBe(LABELS.DISABLED_NO_PROJECT);
+
+ vm.$store.commit(SET_PROJECT, selectedProjectMock);
+ vm.$store.commit(SET_PROJECT_BILLING_STATUS, true);
+ vm.$store.commit(SET_ZONE, selectedZoneMock);
+
+ expect(vm.toggleText).toBe(LABELS.DEFAULT);
+ });
+
+ it('returns machine type name if machine type selected', () => {
+ vm.setItem(selectedMachineTypeMock);
+
+ expect(vm.toggleText).toBe(selectedMachineTypeMock);
+ });
+ });
+
+ describe('form input', () => {
+ it('reflects new value when dropdown item is clicked', done => {
+ expect(vm.$el.querySelector('input').value).toBe('');
+ vm.$store.commit(SET_MACHINE_TYPES, gapiMachineTypesResponseMock.items);
+
+ return vm.$nextTick().then(() => {
+ vm.$el.querySelector('.dropdown-content button').click();
+
+ return vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('input').value).toBe(selectedMachineTypeMock);
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown_spec.js b/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown_spec.js
new file mode 100644
index 00000000000..d4fcb2dc8ff
--- /dev/null
+++ b/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown_spec.js
@@ -0,0 +1,92 @@
+import Vue from 'vue';
+import GkeProjectIdDropdown from '~/projects/gke_cluster_dropdowns/components/gke_project_id_dropdown.vue';
+import { createStore } from '~/projects/gke_cluster_dropdowns/store';
+import { SET_PROJECTS } from '~/projects/gke_cluster_dropdowns/store/mutation_types';
+import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { emptyProjectMock, selectedProjectMock } from '../mock_data';
+
+const componentConfig = {
+ docsUrl: 'https://console.cloud.google.com/home/dashboard',
+ fieldId: 'cluster_provider_gcp_attributes_gcp_project_id',
+ fieldName: 'cluster[provider_gcp_attributes][gcp_project_id]',
+};
+
+const LABELS = {
+ LOADING: 'Fetching projects',
+ VALIDATING_PROJECT_BILLING: 'Validating project billing status',
+ DEFAULT: 'Select project',
+ EMPTY: 'No projects found',
+};
+
+const createComponent = (store, props = componentConfig) => {
+ const Component = Vue.extend(GkeProjectIdDropdown);
+
+ return mountComponentWithStore(Component, {
+ el: null,
+ props,
+ store,
+ });
+};
+
+describe('GkeProjectIdDropdown', () => {
+ let vm;
+ let store;
+
+ beforeEach(() => {
+ store = createStore();
+ vm = createComponent(store);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('toggleText', () => {
+ it('returns loading toggle text', () => {
+ expect(vm.toggleText).toBe(LABELS.LOADING);
+ });
+
+ it('returns project billing validation text', () => {
+ vm.setIsValidatingProjectBilling(true);
+ expect(vm.toggleText).toBe(LABELS.VALIDATING_PROJECT_BILLING);
+ });
+
+ it('returns default toggle text', done =>
+ vm.$nextTick().then(() => {
+ vm.setItem(emptyProjectMock);
+
+ expect(vm.toggleText).toBe(LABELS.DEFAULT);
+ done();
+ }));
+
+ it('returns project name if project selected', done =>
+ vm.$nextTick().then(() => {
+ expect(vm.toggleText).toBe(selectedProjectMock.name);
+ done();
+ }));
+
+ it('returns empty toggle text', done =>
+ vm.$nextTick().then(() => {
+ vm.$store.commit(SET_PROJECTS, null);
+ vm.setItem(emptyProjectMock);
+
+ expect(vm.toggleText).toBe(LABELS.EMPTY);
+ done();
+ }));
+ });
+
+ describe('selectItem', () => {
+ it('reflects new value when dropdown item is clicked', done => {
+ expect(vm.$el.querySelector('input').value).toBe('');
+
+ return vm.$nextTick().then(() => {
+ vm.$el.querySelector('.dropdown-content button').click();
+
+ return vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('input').value).toBe(selectedProjectMock.projectId);
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown_spec.js b/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown_spec.js
new file mode 100644
index 00000000000..89a4a7ea2ce
--- /dev/null
+++ b/spec/javascripts/projects/gke_cluster_dropdowns/components/gke_zone_dropdown_spec.js
@@ -0,0 +1,88 @@
+import Vue from 'vue';
+import GkeZoneDropdown from '~/projects/gke_cluster_dropdowns/components/gke_zone_dropdown.vue';
+import { createStore } from '~/projects/gke_cluster_dropdowns/store';
+import {
+ SET_PROJECT,
+ SET_ZONES,
+ SET_PROJECT_BILLING_STATUS,
+} from '~/projects/gke_cluster_dropdowns/store/mutation_types';
+import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
+import { selectedZoneMock, selectedProjectMock, gapiZonesResponseMock } from '../mock_data';
+
+const componentConfig = {
+ fieldId: 'cluster_provider_gcp_attributes_gcp_zone',
+ fieldName: 'cluster[provider_gcp_attributes][gcp_zone]',
+};
+
+const LABELS = {
+ LOADING: 'Fetching zones',
+ DISABLED: 'Select project to choose zone',
+ DEFAULT: 'Select zone',
+};
+
+const createComponent = (store, props = componentConfig) => {
+ const Component = Vue.extend(GkeZoneDropdown);
+
+ return mountComponentWithStore(Component, {
+ el: null,
+ props,
+ store,
+ });
+};
+
+describe('GkeZoneDropdown', () => {
+ let vm;
+ let store;
+
+ beforeEach(() => {
+ store = createStore();
+ vm = createComponent(store);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('toggleText', () => {
+ it('returns disabled state toggle text', () => {
+ expect(vm.toggleText).toBe(LABELS.DISABLED);
+ });
+
+ it('returns loading toggle text', () => {
+ vm.isLoading = true;
+
+ expect(vm.toggleText).toBe(LABELS.LOADING);
+ });
+
+ it('returns default toggle text', () => {
+ expect(vm.toggleText).toBe(LABELS.DISABLED);
+
+ vm.$store.commit(SET_PROJECT, selectedProjectMock);
+ vm.$store.commit(SET_PROJECT_BILLING_STATUS, true);
+
+ expect(vm.toggleText).toBe(LABELS.DEFAULT);
+ });
+
+ it('returns project name if project selected', () => {
+ vm.setItem(selectedZoneMock);
+
+ expect(vm.toggleText).toBe(selectedZoneMock);
+ });
+ });
+
+ describe('selectItem', () => {
+ it('reflects new value when dropdown item is clicked', done => {
+ expect(vm.$el.querySelector('input').value).toBe('');
+ vm.$store.commit(SET_ZONES, gapiZonesResponseMock.items);
+
+ return vm.$nextTick().then(() => {
+ vm.$el.querySelector('.dropdown-content button').click();
+
+ return vm.$nextTick().then(() => {
+ expect(vm.$el.querySelector('input').value).toBe(selectedZoneMock);
+ done();
+ });
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/projects/gke_cluster_dropdowns/helpers.js b/spec/javascripts/projects/gke_cluster_dropdowns/helpers.js
new file mode 100644
index 00000000000..6df511e9157
--- /dev/null
+++ b/spec/javascripts/projects/gke_cluster_dropdowns/helpers.js
@@ -0,0 +1,49 @@
+import {
+ gapiProjectsResponseMock,
+ gapiZonesResponseMock,
+ gapiMachineTypesResponseMock,
+} from './mock_data';
+
+// eslint-disable-next-line import/prefer-default-export
+export const gapi = () => ({
+ client: {
+ cloudbilling: {
+ projects: {
+ getBillingInfo: () =>
+ new Promise(resolve => {
+ resolve({
+ result: { billingEnabled: true },
+ });
+ }),
+ },
+ },
+ cloudresourcemanager: {
+ projects: {
+ list: () =>
+ new Promise(resolve => {
+ resolve({
+ result: { ...gapiProjectsResponseMock },
+ });
+ }),
+ },
+ },
+ compute: {
+ zones: {
+ list: () =>
+ new Promise(resolve => {
+ resolve({
+ result: { ...gapiZonesResponseMock },
+ });
+ }),
+ },
+ machineTypes: {
+ list: () =>
+ new Promise(resolve => {
+ resolve({
+ result: { ...gapiMachineTypesResponseMock },
+ });
+ }),
+ },
+ },
+ },
+});
diff --git a/spec/javascripts/projects/gke_cluster_dropdowns/mock_data.js b/spec/javascripts/projects/gke_cluster_dropdowns/mock_data.js
new file mode 100644
index 00000000000..d9f5dbc636f
--- /dev/null
+++ b/spec/javascripts/projects/gke_cluster_dropdowns/mock_data.js
@@ -0,0 +1,75 @@
+export const emptyProjectMock = {
+ projectId: '',
+ name: '',
+};
+
+export const selectedProjectMock = {
+ projectId: 'gcp-project-123',
+ name: 'gcp-project',
+};
+
+export const selectedZoneMock = 'us-central1-a';
+
+export const selectedMachineTypeMock = 'n1-standard-2';
+
+export const gapiProjectsResponseMock = {
+ projects: [
+ {
+ projectNumber: '1234',
+ projectId: 'gcp-project-123',
+ lifecycleState: 'ACTIVE',
+ name: 'gcp-project',
+ createTime: '2017-12-16T01:48:29.129Z',
+ parent: {
+ type: 'organization',
+ id: '12345',
+ },
+ },
+ ],
+};
+
+export const gapiZonesResponseMock = {
+ kind: 'compute#zoneList',
+ id: 'projects/gitlab-internal-153318/zones',
+ items: [
+ {
+ kind: 'compute#zone',
+ id: '2000',
+ creationTimestamp: '1969-12-31T16:00:00.000-08:00',
+ name: 'us-central1-a',
+ description: 'us-central1-a',
+ status: 'UP',
+ region:
+ 'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/regions/us-central1',
+ selfLink:
+ 'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/zones/us-central1-a',
+ availableCpuPlatforms: ['Intel Skylake', 'Intel Broadwell', 'Intel Sandy Bridge'],
+ },
+ ],
+ selfLink: 'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/zones',
+};
+
+export const gapiMachineTypesResponseMock = {
+ kind: 'compute#machineTypeList',
+ id: 'projects/gitlab-internal-153318/zones/us-central1-a/machineTypes',
+ items: [
+ {
+ kind: 'compute#machineType',
+ id: '3002',
+ creationTimestamp: '1969-12-31T16:00:00.000-08:00',
+ name: 'n1-standard-2',
+ description: '2 vCPUs, 7.5 GB RAM',
+ guestCpus: 2,
+ memoryMb: 7680,
+ imageSpaceGb: 10,
+ maximumPersistentDisks: 64,
+ maximumPersistentDisksSizeGb: '65536',
+ zone: 'us-central1-a',
+ selfLink:
+ 'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/zones/us-central1-a/machineTypes/n1-standard-2',
+ isSharedCpu: false,
+ },
+ ],
+ selfLink:
+ 'https://www.googleapis.com/compute/v1/projects/gitlab-internal-153318/zones/us-central1-a/machineTypes',
+};
diff --git a/spec/javascripts/projects/gke_cluster_dropdowns/stores/actions_spec.js b/spec/javascripts/projects/gke_cluster_dropdowns/stores/actions_spec.js
new file mode 100644
index 00000000000..9d892b8185b
--- /dev/null
+++ b/spec/javascripts/projects/gke_cluster_dropdowns/stores/actions_spec.js
@@ -0,0 +1,131 @@
+import testAction from 'spec/helpers/vuex_action_helper';
+import * as actions from '~/projects/gke_cluster_dropdowns/store/actions';
+import { createStore } from '~/projects/gke_cluster_dropdowns/store';
+import { gapi } from '../helpers';
+import { selectedProjectMock, selectedZoneMock, selectedMachineTypeMock } from '../mock_data';
+
+describe('GCP Cluster Dropdown Store Actions', () => {
+ let store;
+
+ beforeEach(() => {
+ store = createStore();
+ });
+
+ describe('setProject', () => {
+ it('should set project', done => {
+ testAction(
+ actions.setProject,
+ selectedProjectMock,
+ { selectedProject: {} },
+ [{ type: 'SET_PROJECT', payload: selectedProjectMock }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setZone', () => {
+ it('should set zone', done => {
+ testAction(
+ actions.setZone,
+ selectedZoneMock,
+ { selectedZone: '' },
+ [{ type: 'SET_ZONE', payload: selectedZoneMock }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setMachineType', () => {
+ it('should set machine type', done => {
+ testAction(
+ actions.setMachineType,
+ selectedMachineTypeMock,
+ { selectedMachineType: '' },
+ [{ type: 'SET_MACHINE_TYPE', payload: selectedMachineTypeMock }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setIsValidatingProjectBilling', () => {
+ it('should set machine type', done => {
+ testAction(
+ actions.setIsValidatingProjectBilling,
+ true,
+ { isValidatingProjectBilling: null },
+ [{ type: 'SET_IS_VALIDATING_PROJECT_BILLING', payload: true }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('async fetch methods', () => {
+ window.gapi = gapi();
+
+ describe('fetchProjects', () => {
+ it('fetches projects from Google API', done => {
+ store
+ .dispatch('fetchProjects')
+ .then(() => {
+ expect(store.state.projects[0].projectId).toEqual(selectedProjectMock.projectId);
+ expect(store.state.projects[0].name).toEqual(selectedProjectMock.name);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('validateProjectBilling', () => {
+ it('checks project billing status from Google API', done => {
+ testAction(
+ actions.validateProjectBilling,
+ true,
+ {
+ selectedProject: selectedProjectMock,
+ selectedZone: '',
+ selectedMachineType: '',
+ projectHasBillingEnabled: null,
+ },
+ [
+ { type: 'SET_ZONE', payload: '' },
+ { type: 'SET_MACHINE_TYPE', payload: '' },
+ { type: 'SET_PROJECT_BILLING_STATUS', payload: true },
+ ],
+ [{ type: 'setIsValidatingProjectBilling', payload: false }],
+ done,
+ );
+ });
+ });
+
+ describe('fetchZones', () => {
+ it('fetches zones from Google API', done => {
+ store
+ .dispatch('fetchZones')
+ .then(() => {
+ expect(store.state.zones[0].name).toEqual(selectedZoneMock);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('fetchMachineTypes', () => {
+ it('fetches machine types from Google API', done => {
+ store
+ .dispatch('fetchMachineTypes')
+ .then(() => {
+ expect(store.state.machineTypes[0].name).toEqual(selectedMachineTypeMock);
+
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/projects/gke_cluster_dropdowns/stores/getters_spec.js b/spec/javascripts/projects/gke_cluster_dropdowns/stores/getters_spec.js
new file mode 100644
index 00000000000..6f89158f807
--- /dev/null
+++ b/spec/javascripts/projects/gke_cluster_dropdowns/stores/getters_spec.js
@@ -0,0 +1,65 @@
+import * as getters from '~/projects/gke_cluster_dropdowns/store/getters';
+import { selectedProjectMock, selectedZoneMock, selectedMachineTypeMock } from '../mock_data';
+
+describe('GCP Cluster Dropdown Store Getters', () => {
+ let state;
+
+ describe('valid states', () => {
+ beforeEach(() => {
+ state = {
+ selectedProject: selectedProjectMock,
+ selectedZone: selectedZoneMock,
+ selectedMachineType: selectedMachineTypeMock,
+ };
+ });
+
+ describe('hasProject', () => {
+ it('should return true when project is selected', () => {
+ expect(getters.hasProject(state)).toEqual(true);
+ });
+ });
+
+ describe('hasZone', () => {
+ it('should return true when zone is selected', () => {
+ expect(getters.hasZone(state)).toEqual(true);
+ });
+ });
+
+ describe('hasMachineType', () => {
+ it('should return true when machine type is selected', () => {
+ expect(getters.hasMachineType(state)).toEqual(true);
+ });
+ });
+ });
+
+ describe('invalid states', () => {
+ beforeEach(() => {
+ state = {
+ selectedProject: {
+ projectId: '',
+ name: '',
+ },
+ selectedZone: '',
+ selectedMachineType: '',
+ };
+ });
+
+ describe('hasProject', () => {
+ it('should return false when project is not selected', () => {
+ expect(getters.hasProject(state)).toEqual(false);
+ });
+ });
+
+ describe('hasZone', () => {
+ it('should return false when zone is not selected', () => {
+ expect(getters.hasZone(state)).toEqual(false);
+ });
+ });
+
+ describe('hasMachineType', () => {
+ it('should return false when machine type is not selected', () => {
+ expect(getters.hasMachineType(state)).toEqual(false);
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/projects/gke_cluster_dropdowns/stores/mutations_spec.js b/spec/javascripts/projects/gke_cluster_dropdowns/stores/mutations_spec.js
new file mode 100644
index 00000000000..7f8c4f314e4
--- /dev/null
+++ b/spec/javascripts/projects/gke_cluster_dropdowns/stores/mutations_spec.js
@@ -0,0 +1,87 @@
+import { createStore } from '~/projects/gke_cluster_dropdowns/store';
+import * as types from '~/projects/gke_cluster_dropdowns/store/mutation_types';
+import {
+ selectedProjectMock,
+ selectedZoneMock,
+ selectedMachineTypeMock,
+ gapiProjectsResponseMock,
+ gapiZonesResponseMock,
+ gapiMachineTypesResponseMock,
+} from '../mock_data';
+
+describe('GCP Cluster Dropdown Store Mutations', () => {
+ let store;
+
+ beforeEach(() => {
+ store = createStore();
+ });
+
+ describe('SET_PROJECT', () => {
+ it('should set GCP project as selectedProject', () => {
+ const projectToSelect = gapiProjectsResponseMock.projects[0];
+
+ store.commit(types.SET_PROJECT, projectToSelect);
+
+ expect(store.state.selectedProject.projectId).toEqual(selectedProjectMock.projectId);
+ expect(store.state.selectedProject.name).toEqual(selectedProjectMock.name);
+ });
+ });
+
+ describe('SET_PROJECT_BILLING_STATUS', () => {
+ it('should set project billing status', () => {
+ store.commit(types.SET_PROJECT_BILLING_STATUS, true);
+
+ expect(store.state.projectHasBillingEnabled).toBeTruthy();
+ });
+ });
+
+ describe('SET_ZONE', () => {
+ it('should set GCP zone as selectedZone', () => {
+ const zoneToSelect = gapiZonesResponseMock.items[0].name;
+
+ store.commit(types.SET_ZONE, zoneToSelect);
+
+ expect(store.state.selectedZone).toEqual(selectedZoneMock);
+ });
+ });
+
+ describe('SET_MACHINE_TYPE', () => {
+ it('should set GCP machine type as selectedMachineType', () => {
+ const machineTypeToSelect = gapiMachineTypesResponseMock.items[0].name;
+
+ store.commit(types.SET_MACHINE_TYPE, machineTypeToSelect);
+
+ expect(store.state.selectedMachineType).toEqual(selectedMachineTypeMock);
+ });
+ });
+
+ describe('SET_PROJECTS', () => {
+ it('should set Google API Projects response as projects', () => {
+ expect(store.state.projects.length).toEqual(0);
+
+ store.commit(types.SET_PROJECTS, gapiProjectsResponseMock.projects);
+
+ expect(store.state.projects.length).toEqual(gapiProjectsResponseMock.projects.length);
+ });
+ });
+
+ describe('SET_ZONES', () => {
+ it('should set Google API Zones response as zones', () => {
+ expect(store.state.zones.length).toEqual(0);
+
+ store.commit(types.SET_ZONES, gapiZonesResponseMock.items);
+
+ expect(store.state.zones.length).toEqual(gapiZonesResponseMock.items.length);
+ });
+ });
+
+ describe('SET_MACHINE_TYPES', () => {
+ it('should set Google API Machine Types response as machineTypes', () => {
+ expect(store.state.machineTypes.length).toEqual(0);
+
+ store.commit(types.SET_MACHINE_TYPES, gapiMachineTypesResponseMock.items);
+
+ expect(store.state.machineTypes.length).toEqual(gapiMachineTypesResponseMock.items.length);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js b/spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js
new file mode 100644
index 00000000000..ba897f4660d
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/dropdown/dropdown_button_spec.js
@@ -0,0 +1,69 @@
+import Vue from 'vue';
+
+import dropdownButtonComponent from '~/vue_shared/components/dropdown/dropdown_button.vue';
+
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+
+const defaultLabel = 'Select';
+const customLabel = 'Select project';
+
+const createComponent = config => {
+ const Component = Vue.extend(dropdownButtonComponent);
+
+ return mountComponent(Component, config);
+};
+
+describe('DropdownButtonComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('dropdownToggleText', () => {
+ it('returns default toggle text', () => {
+ expect(vm.toggleText).toBe(defaultLabel);
+ });
+
+ it('returns custom toggle text when provided via props', () => {
+ const vmEmptyLabels = createComponent({ toggleText: customLabel });
+
+ expect(vmEmptyLabels.toggleText).toBe(customLabel);
+ vmEmptyLabels.$destroy();
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component container element of type `button`', () => {
+ expect(vm.$el.nodeName).toBe('BUTTON');
+ });
+
+ it('renders component container element with required data attributes', () => {
+ expect(vm.$el.dataset.abilityName).toBe(vm.abilityName);
+ expect(vm.$el.dataset.fieldName).toBe(vm.fieldName);
+ expect(vm.$el.dataset.issueUpdate).toBe(vm.updatePath);
+ expect(vm.$el.dataset.labels).toBe(vm.labelsPath);
+ expect(vm.$el.dataset.namespacePath).toBe(vm.namespace);
+ expect(vm.$el.dataset.showAny).not.toBeDefined();
+ });
+
+ it('renders dropdown toggle text element', () => {
+ const dropdownToggleTextEl = vm.$el.querySelector('.dropdown-toggle-text');
+ expect(dropdownToggleTextEl).not.toBeNull();
+ expect(dropdownToggleTextEl.innerText.trim()).toBe(defaultLabel);
+ });
+
+ it('renders dropdown button icon', () => {
+ const dropdownIconEl = vm.$el.querySelector('.dropdown-toggle-icon i.fa');
+
+ expect(dropdownIconEl).not.toBeNull();
+ expect(dropdownIconEl.classList.contains('fa-chevron-down')).toBe(true);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js b/spec/javascripts/vue_shared/components/dropdown/dropdown_hidden_input_spec.js
index 88733922a59..445ab0cb40e 100644
--- a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js
+++ b/spec/javascripts/vue_shared/components/dropdown/dropdown_hidden_input_spec.js
@@ -1,17 +1,17 @@
import Vue from 'vue';
-import dropdownHiddenInputComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue';
+import dropdownHiddenInputComponent from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockLabels } from './mock_data';
-const createComponent = (name = 'label_id[]', label = mockLabels[0]) => {
+const createComponent = (name = 'label_id[]', value = mockLabels[0].id) => {
const Component = Vue.extend(dropdownHiddenInputComponent);
return mountComponent(Component, {
name,
- label,
+ value,
});
};
@@ -31,7 +31,7 @@ describe('DropdownHiddenInputComponent', () => {
expect(vm.$el.nodeName).toBe('INPUT');
expect(vm.$el.getAttribute('type')).toBe('hidden');
expect(vm.$el.getAttribute('name')).toBe(vm.name);
- expect(vm.$el.getAttribute('value')).toBe(`${vm.label.id}`);
+ expect(vm.$el.getAttribute('value')).toBe(`${vm.value}`);
});
});
});
diff --git a/spec/javascripts/vue_shared/components/dropdown/dropdown_search_input_spec.js b/spec/javascripts/vue_shared/components/dropdown/dropdown_search_input_spec.js
new file mode 100644
index 00000000000..551520721e5
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/dropdown/dropdown_search_input_spec.js
@@ -0,0 +1,52 @@
+import Vue from 'vue';
+
+import dropdownSearchInputComponent from '~/vue_shared/components/dropdown/dropdown_search_input.vue';
+
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+
+const componentConfig = {
+ placeholderText: 'Search something',
+};
+
+const createComponent = (config = componentConfig) => {
+ const Component = Vue.extend(dropdownSearchInputComponent);
+
+ return mountComponent(Component, config);
+};
+
+describe('DropdownSearchInputComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('template', () => {
+ it('renders input element with type `search`', () => {
+ const inputEl = vm.$el.querySelector('input.dropdown-input-field');
+
+ expect(inputEl).not.toBeNull();
+ expect(inputEl.getAttribute('type')).toBe('search');
+ });
+
+ it('renders search icon element', () => {
+ expect(vm.$el.querySelector('.fa-search.dropdown-input-search')).not.toBeNull();
+ });
+
+ it('renders clear search icon element', () => {
+ expect(
+ vm.$el.querySelector('.fa-times.dropdown-input-clear.js-dropdown-input-clear'),
+ ).not.toBeNull();
+ });
+
+ it('displays custom placeholder text', () => {
+ const inputEl = vm.$el.querySelector('input.dropdown-input-field');
+
+ expect(inputEl.getAttribute('placeholder')).toBe(componentConfig.placeholderText);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/dropdown/mock_data.js b/spec/javascripts/vue_shared/components/dropdown/mock_data.js
new file mode 100644
index 00000000000..b09d42da401
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/dropdown/mock_data.js
@@ -0,0 +1,11 @@
+export const mockLabels = [
+ {
+ id: 26,
+ title: 'Foo Label',
+ description: 'Foobar',
+ color: '#BADA55',
+ text_color: '#FFFFFF',
+ },
+];
+
+export default mockLabels;
diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb
index a44243ac82d..023bedaaebb 100644
--- a/spec/lib/backup/repository_spec.rb
+++ b/spec/lib/backup/repository_spec.rb
@@ -71,6 +71,40 @@ describe Backup::Repository do
end
end
+ describe '#delete_all_repositories', :seed_helper do
+ shared_examples('delete_all_repositories') do
+ before do
+ allow(FileUtils).to receive(:mkdir_p).and_call_original
+ allow(FileUtils).to receive(:mv).and_call_original
+ end
+
+ after(:all) do
+ ensure_seeds
+ end
+
+ it 'removes all repositories' do
+ # Sanity check: there should be something for us to delete
+ expect(list_repositories).to include(File.join(SEED_STORAGE_PATH, TEST_REPO_PATH))
+
+ subject.delete_all_repositories('default', Gitlab.config.repositories.storages['default'])
+
+ expect(list_repositories).to be_empty
+ end
+
+ def list_repositories
+ Dir[SEED_STORAGE_PATH + '/*.git']
+ end
+ end
+
+ context 'with gitaly' do
+ it_behaves_like 'delete_all_repositories'
+ end
+
+ context 'without gitaly', :skip_gitaly_mock do
+ it_behaves_like 'delete_all_repositories'
+ end
+ end
+
describe '#empty_repo?' do
context 'for a wiki' do
let(:wiki) { create(:project_wiki) }
diff --git a/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb b/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb
index fa209bed74e..002ce776be9 100644
--- a/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb
+++ b/spec/lib/gitlab/auth/user_access_denied_reason_spec.rb
@@ -22,7 +22,8 @@ describe Gitlab::Auth::UserAccessDeniedReason do
enforce_terms
end
- it { is_expected.to match /You must accept the Terms of Service/ }
+ it { is_expected.to match /must accept the Terms of Service/ }
+ it { is_expected.to include(user.username) }
end
context 'when the user is internal' do
diff --git a/spec/lib/gitlab/database/count_spec.rb b/spec/lib/gitlab/database/count_spec.rb
index 9d9caaabe16..407d9470785 100644
--- a/spec/lib/gitlab/database/count_spec.rb
+++ b/spec/lib/gitlab/database/count_spec.rb
@@ -3,59 +3,68 @@ require 'spec_helper'
describe Gitlab::Database::Count do
before do
create_list(:project, 3)
+ create(:identity)
end
- describe '.execute_estimate_if_updated_recently', :postgresql do
- context 'when reltuples have not been updated' do
- before do
- expect(described_class).to receive(:reltuples_updated_recently?).and_return(false)
- end
+ let(:models) { [Project, Identity] }
- it 'returns nil' do
- expect(described_class.execute_estimate_if_updated_recently(Project)).to be nil
- end
- end
+ describe '.approximate_counts' do
+ context 'with MySQL' do
+ context 'when reltuples have not been updated' do
+ it 'counts all models the normal way' do
+ expect(Gitlab::Database).to receive(:postgresql?).and_return(false)
- context 'when reltuples have been updated' do
- before do
- ActiveRecord::Base.connection.execute('ANALYZE projects')
- end
+ expect(Project).to receive(:count).and_call_original
+ expect(Identity).to receive(:count).and_call_original
- it 'calls postgresql_estimate_query' do
- expect(described_class).to receive(:postgresql_estimate_query).with(Project).and_call_original
- expect(described_class.execute_estimate_if_updated_recently(Project)).to eq(3)
+ expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 })
+ end
end
end
- end
- describe '.approximate_count' do
- context 'when reltuples have not been updated' do
- it 'counts all projects the normal way' do
- allow(described_class).to receive(:reltuples_updated_recently?).and_return(false)
+ context 'with PostgreSQL', :postgresql do
+ describe 'when reltuples have not been updated' do
+ it 'counts all models the normal way' do
+ expect(described_class).to receive(:reltuples_from_recently_updated).with(%w(projects identities)).and_return({})
- expect(Project).to receive(:count).and_call_original
- expect(described_class.approximate_count(Project)).to eq(3)
+ expect(Project).to receive(:count).and_call_original
+ expect(Identity).to receive(:count).and_call_original
+ expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 })
+ end
end
- end
- context 'no permission' do
- it 'falls back to standard query' do
- allow(described_class).to receive(:reltuples_updated_recently?).and_raise(PG::InsufficientPrivilege)
+ describe 'no permission' do
+ it 'falls back to standard query' do
+ allow(described_class).to receive(:postgresql_estimate_query).and_raise(PG::InsufficientPrivilege)
- expect(Project).to receive(:count).and_call_original
- expect(described_class.approximate_count(Project)).to eq(3)
+ expect(Project).to receive(:count).and_call_original
+ expect(Identity).to receive(:count).and_call_original
+ expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 })
+ end
end
- end
- describe 'when reltuples have been updated', :postgresql do
- before do
- ActiveRecord::Base.connection.execute('ANALYZE projects')
+ describe 'when some reltuples have been updated' do
+ it 'counts projects in the fast way' do
+ expect(described_class).to receive(:reltuples_from_recently_updated).with(%w(projects identities)).and_return({ 'projects' => 3 })
+
+ expect(Project).not_to receive(:count).and_call_original
+ expect(Identity).to receive(:count).and_call_original
+ expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 })
+ end
end
- it 'counts all projects in the fast way' do
- expect(described_class).to receive(:postgresql_estimate_query).with(Project).and_call_original
+ describe 'when all reltuples have been updated' do
+ before do
+ ActiveRecord::Base.connection.execute('ANALYZE projects')
+ ActiveRecord::Base.connection.execute('ANALYZE identities')
+ end
+
+ it 'counts models with the standard way' do
+ expect(Project).not_to receive(:count)
+ expect(Identity).not_to receive(:count)
- expect(described_class.approximate_count(Project)).to eq(3)
+ expect(described_class.approximate_counts(models)).to eq({ Project => 3, Identity => 1 })
+ end
end
end
end
diff --git a/spec/lib/gitlab/diff/file_spec.rb b/spec/lib/gitlab/diff/file_spec.rb
index 0c2e18c268a..0588fe935c3 100644
--- a/spec/lib/gitlab/diff/file_spec.rb
+++ b/spec/lib/gitlab/diff/file_spec.rb
@@ -468,4 +468,58 @@ describe Gitlab::Diff::File do
end
end
end
+
+ describe '#diff_hunk' do
+ let(:raw_diff) do
+ <<EOS
+@@ -6,12 +6,18 @@ module Popen
+
+ def popen(cmd, path=nil)
+ unless cmd.is_a?(Array)
+- raise "System commands must be given as an array of strings"
++ raise RuntimeError, "System commands must be given as an array of strings"
+ end
+
+ path ||= Dir.pwd
+- vars = { "PWD" => path }
+- options = { chdir: path }
++
++ vars = {
++ "PWD" => path
++ }
++
++ options = {
++ chdir: path
++ }
+
+ unless File.directory?(path)
+ FileUtils.mkdir_p(path)
+@@ -19,6 +25,7 @@ module Popen
+
+ @cmd_output = ""
+ @cmd_status = 0
++
+ Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
+ @cmd_output << stdout.read
+ @cmd_output << stderr.read
+EOS
+ end
+
+ it 'returns raw diff up to given line index' do
+ allow(diff_file).to receive(:raw_diff) { raw_diff }
+ diff_line = instance_double(Gitlab::Diff::Line, index: 5)
+
+ diff_hunk = <<EOS
+@@ -6,12 +6,18 @@ module Popen
+
+ def popen(cmd, path=nil)
+ unless cmd.is_a?(Array)
+- raise "System commands must be given as an array of strings"
++ raise RuntimeError, "System commands must be given as an array of strings"
+ end
+EOS
+
+ expect(diff_file.diff_hunk(diff_line)).to eq(diff_hunk)
+ end
+ end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 317a932d5a6..dfffea7797f 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -1055,7 +1055,7 @@ describe Gitlab::GitAccess do
it 'blocks access when the user did not accept terms', :aggregate_failures do
actions.each do |action|
- expect { action.call }.to raise_unauthorized(/You must accept the Terms of Service in order to perform this action/)
+ expect { action.call }.to raise_unauthorized(/must accept the Terms of Service in order to perform this action/)
end
end
diff --git a/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb b/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb
new file mode 100644
index 00000000000..f47b9dd3498
--- /dev/null
+++ b/spec/lib/gitlab/grape_logging/loggers/queue_duration_logger_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Gitlab::GrapeLogging::Loggers::QueueDurationLogger do
+ subject { described_class.new }
+
+ describe ".parameters" do
+ let(:start_time) { Time.new(2018, 01, 01) }
+
+ describe 'when no proxy time is available' do
+ let(:mock_request) { OpenStruct.new(env: {}) }
+
+ it 'returns an empty hash' do
+ expect(subject.parameters(mock_request, nil)).to eq({})
+ end
+ end
+
+ describe 'when a proxy time is available' do
+ let(:mock_request) do
+ OpenStruct.new(
+ env: {
+ 'HTTP_GITLAB_WORKHORSE_PROXY_START' => (start_time - 1.hour).to_i * (10**9)
+ }
+ )
+ end
+
+ it 'returns the correct duration in ms' do
+ Timecop.freeze(start_time) do
+ subject.before
+
+ expect(subject.parameters(mock_request, nil)).to eq( { 'queue_duration': 1.hour.to_f * 1000 })
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 8b46b04b8b5..fb5fd300dbb 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -35,6 +35,7 @@ notes:
- todos
- events
- system_note_metadata
+- note_diff_file
label_links:
- target
- label
diff --git a/spec/lib/google_api/cloud_platform/client_spec.rb b/spec/lib/google_api/cloud_platform/client_spec.rb
index db9d9158b29..27cb3198e5b 100644
--- a/spec/lib/google_api/cloud_platform/client_spec.rb
+++ b/spec/lib/google_api/cloud_platform/client_spec.rb
@@ -50,30 +50,6 @@ describe GoogleApi::CloudPlatform::Client do
end
end
- describe '#projects_list' do
- subject { client.projects_list }
- let(:projects) { double }
-
- before do
- allow_any_instance_of(Google::Apis::CloudresourcemanagerV1::CloudResourceManagerService)
- .to receive(:fetch_all).and_return(projects)
- end
-
- it { is_expected.to eq(projects) }
- end
-
- describe '#projects_get_billing_info' do
- subject { client.projects_get_billing_info('project') }
- let(:billing_info) { double }
-
- before do
- allow_any_instance_of(Google::Apis::CloudbillingV1::CloudbillingService)
- .to receive(:get_project_billing_info).and_return(billing_info)
- end
-
- it { is_expected.to eq(billing_info) }
- end
-
describe '#projects_zones_clusters_get' do
subject { client.projects_zones_clusters_get(spy, spy, spy) }
let(:gke_cluster) { double }
diff --git a/spec/migrations/fill_file_store_spec.rb b/spec/migrations/fill_file_store_spec.rb
new file mode 100644
index 00000000000..5ff7aa56ce2
--- /dev/null
+++ b/spec/migrations/fill_file_store_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180424151928_fill_file_store')
+
+describe FillFileStore, :migration do
+ let(:namespaces) { table(:namespaces) }
+ let(:projects) { table(:projects) }
+ let(:builds) { table(:ci_builds) }
+ let(:job_artifacts) { table(:ci_job_artifacts) }
+ let(:lfs_objects) { table(:lfs_objects) }
+ let(:uploads) { table(:uploads) }
+
+ before do
+ namespaces.create!(id: 123, name: 'gitlab1', path: 'gitlab1')
+ projects.create!(id: 123, name: 'gitlab1', path: 'gitlab1', namespace_id: 123)
+ builds.create!(id: 1)
+
+ ##
+ # Create rows that have nullfied `file_store` column
+ job_artifacts.create!(project_id: 123, job_id: 1, file_type: 1, file_store: nil)
+ lfs_objects.create!(oid: 123, size: 10, file: 'file_name', file_store: nil)
+ uploads.create!(size: 10, path: 'path', uploader: 'uploader', mount_point: 'file_name', store: nil)
+ end
+
+ it 'correctly migrates nullified file_store/store column' do
+ expect(job_artifacts.where(file_store: nil).count).to eq(1)
+ expect(lfs_objects.where(file_store: nil).count).to eq(1)
+ expect(uploads.where(store: nil).count).to eq(1)
+
+ expect(job_artifacts.where(file_store: 1).count).to eq(0)
+ expect(lfs_objects.where(file_store: 1).count).to eq(0)
+ expect(uploads.where(store: 1).count).to eq(0)
+
+ migrate!
+
+ expect(job_artifacts.where(file_store: nil).count).to eq(0)
+ expect(lfs_objects.where(file_store: nil).count).to eq(0)
+ expect(uploads.where(store: nil).count).to eq(0)
+
+ expect(job_artifacts.where(file_store: 1).count).to eq(1)
+ expect(lfs_objects.where(file_store: 1).count).to eq(1)
+ expect(uploads.where(store: 1).count).to eq(1)
+ end
+end
diff --git a/spec/models/concerns/discussion_on_diff_spec.rb b/spec/models/concerns/discussion_on_diff_spec.rb
index 30572ce9332..8cd129dc851 100644
--- a/spec/models/concerns/discussion_on_diff_spec.rb
+++ b/spec/models/concerns/discussion_on_diff_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe DiscussionOnDiff do
- subject { create(:diff_note_on_merge_request).to_discussion }
+ subject { create(:diff_note_on_merge_request, line_number: 18).to_discussion }
describe "#truncated_diff_lines" do
let(:truncated_lines) { subject.truncated_diff_lines }
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index fb51c0172ab..8624f0daa4d 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -84,7 +84,47 @@ describe DiffNote do
end
end
- describe "#diff_file" do
+ describe '#create_diff_file callback' do
+ let(:noteable) { create(:merge_request) }
+ let(:project) { noteable.project }
+
+ context 'merge request' do
+ let!(:diff_note) { create(:diff_note_on_merge_request, project: project, noteable: noteable) }
+
+ it 'creates a diff note file' do
+ expect(diff_note.reload.note_diff_file).to be_present
+ end
+
+ it 'does not create diff note file if it is a reply' do
+ expect { create(:diff_note_on_merge_request, noteable: noteable, in_reply_to: diff_note) }
+ .not_to change(NoteDiffFile, :count)
+ end
+ end
+
+ context 'commit' do
+ let!(:diff_note) { create(:diff_note_on_commit, project: project) }
+
+ it 'creates a diff note file' do
+ expect(diff_note.reload.note_diff_file).to be_present
+ end
+
+ it 'does not create diff note file if it is a reply' do
+ expect { create(:diff_note_on_commit, in_reply_to: diff_note) }
+ .not_to change(NoteDiffFile, :count)
+ end
+ end
+ end
+
+ describe '#diff_file', :clean_gitlab_redis_shared_state do
+ context 'when note_diff_file association exists' do
+ it 'returns persisted diff file data' do
+ diff_file = subject.diff_file
+
+ expect(diff_file.diff.to_hash.with_indifferent_access)
+ .to include(subject.note_diff_file.to_hash)
+ end
+ end
+
context 'when the discussion was created in the diff' do
it 'returns correct diff file' do
diff_file = subject.diff_file
@@ -115,6 +155,26 @@ describe DiffNote do
expect(diff_file.diff_refs).to eq(position.diff_refs)
end
end
+
+ context 'note diff file creation enqueuing' do
+ it 'enqueues CreateNoteDiffFileWorker if it is the first note of a discussion' do
+ subject.note_diff_file.destroy!
+
+ expect(CreateNoteDiffFileWorker).to receive(:perform_async).with(subject.id)
+
+ subject.reload.diff_file
+ end
+
+ it 'does not enqueues CreateNoteDiffFileWorker if not first note of a discussion' do
+ mr = create(:merge_request)
+ diff_note = create(:diff_note_on_merge_request, project: mr.project, noteable: mr)
+ reply_diff_note = create(:diff_note_on_merge_request, in_reply_to: diff_note)
+
+ expect(CreateNoteDiffFileWorker).not_to receive(:perform_async).with(reply_diff_note.id)
+
+ reply_diff_note.reload.diff_file
+ end
+ end
end
describe "#diff_line" do
diff --git a/spec/models/internal_id_spec.rb b/spec/models/internal_id_spec.rb
index 8ef91e8fab5..581fd0293cc 100644
--- a/spec/models/internal_id_spec.rb
+++ b/spec/models/internal_id_spec.rb
@@ -5,7 +5,7 @@ describe InternalId do
let(:usage) { :issues }
let(:issue) { build(:issue, project: project) }
let(:scope) { { project: project } }
- let(:init) { ->(s) { s.project.issues.maximum(:iid) } }
+ let(:init) { ->(s) { s.project.issues.size } }
context 'validations' do
it { is_expected.to validate_presence_of(:usage) }
@@ -39,29 +39,6 @@ describe InternalId do
end
end
- context 'with an InternalId record present and existing issues with a higher internal id' do
- # This can happen if the old NonatomicInternalId is still in use
- before do
- issues = Array.new(rand(1..10)).map { create(:issue, project: project) }
-
- issue = issues.last
- issue.iid = issues.map { |i| i.iid }.max + 1
- issue.save
- end
-
- let(:maximum_iid) { project.issues.map { |i| i.iid }.max }
-
- it 'updates last_value to the maximum internal id present' do
- subject
-
- expect(described_class.find_by(project: project, usage: described_class.usages[usage.to_s]).last_value).to eq(maximum_iid + 1)
- end
-
- it 'returns next internal id correctly' do
- expect(subject).to eq(maximum_iid + 1)
- end
- end
-
context 'with concurrent inserts on table' do
it 'looks up the record if it was created concurrently' do
args = { **scope, usage: described_class.usages[usage.to_s] }
@@ -104,8 +81,7 @@ describe InternalId do
describe '#increment_and_save!' do
let(:id) { create(:internal_id) }
- let(:maximum_iid) { nil }
- subject { id.increment_and_save!(maximum_iid) }
+ subject { id.increment_and_save! }
it 'returns incremented iid' do
value = id.last_value
@@ -126,14 +102,5 @@ describe InternalId do
expect(subject).to eq(1)
end
end
-
- context 'with maximum_iid given' do
- let(:id) { create(:internal_id, last_value: 1) }
- let(:maximum_iid) { id.last_value + 10 }
-
- it 'returns maximum_iid instead' do
- expect(subject).to eq(12)
- end
- end
end
end
diff --git a/spec/models/note_diff_file_spec.rb b/spec/models/note_diff_file_spec.rb
new file mode 100644
index 00000000000..591c1a89748
--- /dev/null
+++ b/spec/models/note_diff_file_spec.rb
@@ -0,0 +1,11 @@
+require 'rails_helper'
+
+describe NoteDiffFile do
+ describe 'associations' do
+ it { is_expected.to belong_to(:diff_note) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:diff_note) }
+ end
+end
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index d3ab44c0d7e..d8a51f36dba 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -171,7 +171,7 @@ describe API::Helpers do
end
it 'returns a 403 when a user has not accepted the terms' do
- expect { current_user }.to raise_error /You must accept the Terms of Service/
+ expect { current_user }.to raise_error /must accept the Terms of Service/
end
it 'sets the current user when the user accepted the terms' do
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index db8c5f963d6..5dc3ddd4b36 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
describe API::Internal do
- let(:user) { create(:user) }
+ set(:user) { create(:user) }
let(:key) { create(:key, user: user) }
- let(:project) { create(:project, :repository, :wiki_repo) }
+ set(:project) { create(:project, :repository, :wiki_repo) }
let(:secret_token) { Gitlab::Shell.secret_token }
let(:gl_repository) { "project-#{project.id}" }
let(:reference_counter) { double('ReferenceCounter') }
@@ -277,7 +277,7 @@ describe API::Internal do
expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
- expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo)
+ expect(json_response["repository_path"]).to eq('/')
expect(json_response["gl_repository"]).to eq("wiki-#{project.id}")
expect(user).not_to have_an_activity_record
end
@@ -289,7 +289,7 @@ describe API::Internal do
expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
- expect(json_response["repository_path"]).to eq(project.wiki.repository.path_to_repo)
+ expect(json_response["repository_path"]).to eq('/')
expect(json_response["gl_repository"]).to eq("wiki-#{project.id}")
expect(user).to have_an_activity_record
end
@@ -301,7 +301,7 @@ describe API::Internal do
expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
- expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
+ expect(json_response["repository_path"]).to eq('/')
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
expect(json_response["gitaly"]).not_to be_nil
expect(json_response["gitaly"]["repository"]).not_to be_nil
@@ -320,7 +320,7 @@ describe API::Internal do
expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
- expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
+ expect(json_response["repository_path"]).to eq('/')
expect(json_response["gl_repository"]).to eq("project-#{project.id}")
expect(json_response["gitaly"]).not_to be_nil
expect(json_response["gitaly"]["repository"]).not_to be_nil
diff --git a/spec/requests/api/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index 0a2963452e4..45082e644ca 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -13,7 +13,10 @@ describe API::Jobs do
ref: project.default_branch)
end
- let!(:job) { create(:ci_build, :success, pipeline: pipeline) }
+ let!(:job) do
+ create(:ci_build, :success, pipeline: pipeline,
+ artifacts_expire_at: 1.day.since)
+ end
let(:user) { create(:user) }
let(:api_user) { user }
@@ -43,6 +46,7 @@ describe API::Jobs do
it 'returns correct values' do
expect(json_response).not_to be_empty
expect(json_response.first['commit']['id']).to eq project.commit.id
+ expect(Time.parse(json_response.first['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at)
end
it 'returns pipeline data' do
@@ -128,6 +132,7 @@ describe API::Jobs do
it 'returns correct values' do
expect(json_response).not_to be_empty
expect(json_response.first['commit']['id']).to eq project.commit.id
+ expect(Time.parse(json_response.first['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at)
end
it 'returns pipeline data' do
@@ -201,6 +206,7 @@ describe API::Jobs do
expect(Time.parse(json_response['created_at'])).to be_like_time(job.created_at)
expect(Time.parse(json_response['started_at'])).to be_like_time(job.started_at)
expect(Time.parse(json_response['finished_at'])).to be_like_time(job.finished_at)
+ expect(Time.parse(json_response['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at)
expect(json_response['duration']).to eq(job.duration)
end
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index efb9bddde44..6aadf839dbd 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -830,6 +830,21 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
end
end
+ context 'when job has already been finished' do
+ before do
+ job.trace.set('Job failed')
+ job.drop!(:script_failure)
+ end
+
+ it 'does not update job status and job trace' do
+ update_job(state: 'success', trace: 'BUILD TRACE UPDATED')
+
+ expect(response).to have_gitlab_http_status(403)
+ expect(job.trace.raw).to eq 'Job failed'
+ expect(job).to be_failed
+ end
+ end
+
def update_job(token = job.token, **params)
new_params = params.merge(token: token)
put api("/jobs/#{job.id}"), new_params
@@ -1210,7 +1225,7 @@ describe API::Runner, :clean_gitlab_redis_shared_state do
before do
fog_connection.directories.get('artifacts').files.create(
- key: 'tmp/upload/12312300',
+ key: 'tmp/uploads/12312300',
body: 'content'
)
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index f80abb06fca..79672fe1cc5 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -1089,7 +1089,7 @@ describe 'Git LFS API and storage' do
context 'with valid remote_id' do
before do
fog_connection.directories.get('lfs-objects').files.create(
- key: 'tmp/upload/12312300',
+ key: 'tmp/uploads/12312300',
body: 'content'
)
end
diff --git a/spec/services/check_gcp_project_billing_service_spec.rb b/spec/services/check_gcp_project_billing_service_spec.rb
deleted file mode 100644
index 3e68d906e71..00000000000
--- a/spec/services/check_gcp_project_billing_service_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-require 'spec_helper'
-
-describe CheckGcpProjectBillingService do
- include GoogleApi::CloudPlatformHelpers
-
- let(:service) { described_class.new }
- let(:project_id) { 'test-project-1234' }
-
- describe '#execute' do
- before do
- stub_cloud_platform_projects_list(project_id: project_id)
- end
-
- subject { service.execute('bogustoken') }
-
- context 'google account has a billing enabled gcp project' do
- before do
- stub_cloud_platform_projects_get_billing_info(project_id, true)
- end
-
- it { is_expected.to all(satisfy { |project| project.project_id == project_id }) }
- end
-
- context 'google account does not have a billing enabled gcp project' do
- before do
- stub_cloud_platform_projects_get_billing_info(project_id, false)
- end
-
- it { is_expected.to eq([]) }
- end
- end
-end
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index f5cff66de6d..2b2b983494f 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -57,6 +57,88 @@ describe Notes::CreateService do
end
end
+ context 'note diff file' do
+ let(:project_with_repo) { create(:project, :repository) }
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: project_with_repo,
+ target_project: project_with_repo)
+ end
+ let(:line_number) { 14 }
+ let(:position) do
+ Gitlab::Diff::Position.new(old_path: "files/ruby/popen.rb",
+ new_path: "files/ruby/popen.rb",
+ old_line: nil,
+ new_line: line_number,
+ diff_refs: merge_request.diff_refs)
+ end
+ let(:previous_note) do
+ create(:diff_note_on_merge_request, noteable: merge_request, project: project_with_repo)
+ end
+
+ context 'when eligible to have a note diff file' do
+ let(:new_opts) do
+ opts.merge(in_reply_to_discussion_id: nil,
+ type: 'DiffNote',
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id,
+ position: position.to_h)
+ end
+
+ it 'note is associated with a note diff file' do
+ note = described_class.new(project_with_repo, user, new_opts).execute
+
+ expect(note).to be_persisted
+ expect(note.note_diff_file).to be_present
+ end
+ end
+
+ context 'when DiffNote is a reply' do
+ let(:new_opts) do
+ opts.merge(in_reply_to_discussion_id: previous_note.discussion_id,
+ type: 'DiffNote',
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id,
+ position: position.to_h)
+ end
+
+ it 'note is not associated with a note diff file' do
+ note = described_class.new(project_with_repo, user, new_opts).execute
+
+ expect(note).to be_persisted
+ expect(note.note_diff_file).to be_nil
+ end
+
+ context 'when DiffNote from an image' do
+ let(:image_position) do
+ Gitlab::Diff::Position.new(old_path: "files/images/6049019_460s.jpg",
+ new_path: "files/images/6049019_460s.jpg",
+ width: 100,
+ height: 100,
+ x: 1,
+ y: 100,
+ diff_refs: merge_request.diff_refs,
+ position_type: 'image')
+ end
+
+ let(:new_opts) do
+ opts.merge(in_reply_to_discussion_id: nil,
+ type: 'DiffNote',
+ noteable_type: 'MergeRequest',
+ noteable_id: merge_request.id,
+ position: image_position.to_h)
+ end
+
+ it 'note is not associated with a note diff file' do
+ note = described_class.new(project_with_repo, user, new_opts).execute
+
+ expect(note).to be_persisted
+ expect(note.note_diff_file).to be_nil
+ end
+ end
+ end
+ end
+
context 'note with commands' do
context 'as a user who can update the target' do
context '/close, /label, /assign & /milestone' do
diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb
index e7277b337f6..4165a005063 100644
--- a/spec/uploaders/object_storage_spec.rb
+++ b/spec/uploaders/object_storage_spec.rb
@@ -618,7 +618,7 @@ describe ObjectStorage do
let!(:fog_file) do
fog_connection.directories.get('uploads').files.create(
- key: 'tmp/upload/test/123123',
+ key: 'tmp/uploads/test/123123',
body: 'content'
)
end
diff --git a/spec/views/admin/dashboard/index.html.haml_spec.rb b/spec/views/admin/dashboard/index.html.haml_spec.rb
index 59c777ea338..0e8b7c82d3a 100644
--- a/spec/views/admin/dashboard/index.html.haml_spec.rb
+++ b/spec/views/admin/dashboard/index.html.haml_spec.rb
@@ -4,6 +4,11 @@ describe 'admin/dashboard/index.html.haml' do
include Devise::Test::ControllerHelpers
before do
+ counts = Admin::DashboardController::COUNTED_ITEMS.each_with_object({}) do |item, hash|
+ hash[item] = 100
+ end
+
+ assign(:counts, counts)
assign(:projects, create_list(:project, 1))
assign(:users, create_list(:user, 1))
assign(:groups, create_list(:group, 1))
diff --git a/spec/workers/check_gcp_project_billing_worker_spec.rb b/spec/workers/check_gcp_project_billing_worker_spec.rb
deleted file mode 100644
index 526ecf75921..00000000000
--- a/spec/workers/check_gcp_project_billing_worker_spec.rb
+++ /dev/null
@@ -1,116 +0,0 @@
-require 'spec_helper'
-
-describe CheckGcpProjectBillingWorker do
- describe '.perform' do
- let(:token) { 'bogustoken' }
-
- subject { described_class.new.perform('token_key') }
-
- before do
- allow(described_class).to receive(:get_billing_state)
- allow_any_instance_of(described_class).to receive(:update_billing_change_counter)
- end
-
- context 'when there is a token in redis' do
- before do
- allow(described_class).to receive(:get_session_token).and_return(token)
- end
-
- context 'when there is no lease' do
- before do
- allow_any_instance_of(described_class).to receive(:try_obtain_lease_for).and_return('randomuuid')
- end
-
- it 'calls the service' do
- expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([double])
-
- subject
- end
-
- it 'stores billing status in redis' do
- expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([double])
- expect(described_class).to receive(:set_billing_state).with(token, true)
-
- subject
- end
- end
-
- context 'when there is a lease' do
- before do
- allow_any_instance_of(described_class).to receive(:try_obtain_lease_for).and_return(false)
- end
-
- it 'does not call the service' do
- expect(CheckGcpProjectBillingService).not_to receive(:new)
-
- subject
- end
- end
- end
-
- context 'when there is no token in redis' do
- before do
- allow(described_class).to receive(:get_session_token).and_return(nil)
- end
-
- it 'does not call the service' do
- expect(CheckGcpProjectBillingService).not_to receive(:new)
-
- subject
- end
- end
- end
-
- describe 'billing change counter' do
- subject { described_class.new.perform('token_key') }
-
- before do
- allow(described_class).to receive(:get_session_token).and_return('bogustoken')
- allow_any_instance_of(described_class).to receive(:try_obtain_lease_for).and_return('randomuuid')
- allow(described_class).to receive(:set_billing_state)
- end
-
- context 'when previous state was false' do
- before do
- expect(described_class).to receive(:get_billing_state).and_return(false)
- end
-
- context 'when the current state is false' do
- before do
- expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([])
- end
-
- it 'increments the billing change counter' do
- expect_any_instance_of(described_class).to receive_message_chain(:billing_changed_counter, :increment)
-
- subject
- end
- end
-
- context 'when the current state is true' do
- before do
- expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([double])
- end
-
- it 'increments the billing change counter' do
- expect_any_instance_of(described_class).to receive_message_chain(:billing_changed_counter, :increment)
-
- subject
- end
- end
- end
-
- context 'when previous state was true' do
- before do
- expect(described_class).to receive(:get_billing_state).and_return(true)
- expect(CheckGcpProjectBillingService).to receive_message_chain(:new, :execute).and_return([double])
- end
-
- it 'increment the billing change counter' do
- expect_any_instance_of(described_class).to receive_message_chain(:billing_changed_counter, :increment)
-
- subject
- end
- end
- end
-end
diff --git a/spec/workers/create_note_diff_file_worker_spec.rb b/spec/workers/create_note_diff_file_worker_spec.rb
new file mode 100644
index 00000000000..0ac946a1232
--- /dev/null
+++ b/spec/workers/create_note_diff_file_worker_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe CreateNoteDiffFileWorker do
+ describe '#perform' do
+ let(:diff_note) { create(:diff_note_on_merge_request) }
+
+ it 'creates diff file' do
+ diff_note.note_diff_file.destroy!
+
+ expect_any_instance_of(DiffNote).to receive(:create_diff_file)
+ .and_call_original
+
+ described_class.new.perform(diff_note.id)
+ end
+ end
+end
diff --git a/yarn.lock b/yarn.lock
index af8785bbc66..b5ff26c4016 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -385,14 +385,10 @@ ast-types@0.10.1:
version "0.10.1"
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.10.1.tgz#f52fca9715579a14f841d67d7f8d25432ab6a3dd"
-ast-types@0.11.3:
+ast-types@0.11.3, ast-types@0.x.x:
version "0.11.3"
resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.11.3.tgz#c20757fe72ee71278ea0ff3d87e5c2ca30d9edf8"
-ast-types@0.x.x:
- version "0.11.1"
- resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.11.1.tgz#5bb3a8d5ba292c3f4ae94d46df37afc30300b990"
-
async-each@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
@@ -405,18 +401,12 @@ async@1.x, async@^1.4.0, async@^1.5.0, async@^1.5.2:
version "1.5.2"
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
-async@^2.0.0, async@^2.6.0:
+async@^2.0.0, async@^2.1.4, async@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/async/-/async-2.6.0.tgz#61a29abb6fcc026fea77e56d1c6ec53a795951f4"
dependencies:
lodash "^4.14.0"
-async@^2.1.4:
- version "2.4.1"
- resolved "https://registry.yarnpkg.com/async/-/async-2.4.1.tgz#62a56b279c98a11d0987096a01cc3eeb8eb7bbd7"
- dependencies:
- lodash "^4.14.0"
-
async@~2.1.2:
version "2.1.5"
resolved "https://registry.yarnpkg.com/async/-/async-2.1.5.tgz#e587c68580994ac67fc56ff86d3ac56bdbe810bc"
@@ -1284,9 +1274,9 @@ boom@5.x.x:
dependencies:
hoek "4.x.x"
-bootstrap@4.1:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.0.tgz#110b05c31a236d56dbc9adcda6dd16f53738a28a"
+bootstrap@~4.1.1:
+ version "4.1.1"
+ resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.1.1.tgz#3aec85000fa619085da8d2e4983dfd67cf2114cb"
boxen@^1.2.1:
version "1.3.0"
@@ -1581,7 +1571,7 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
strip-ansi "^3.0.0"
supports-color "^2.0.0"
-chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.4.1:
+chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.2, chalk@^2.4.1:
version "2.4.1"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.1.tgz#18c49ab16a037b6eb0152cc83e3471338215b66e"
dependencies:
@@ -1589,14 +1579,6 @@ chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.4
escape-string-regexp "^1.0.5"
supports-color "^5.3.0"
-chalk@^2.3.2:
- version "2.4.0"
- resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.0.tgz#a060a297a6b57e15b61ca63ce84995daa0fe6e52"
- dependencies:
- ansi-styles "^3.2.1"
- escape-string-regexp "^1.0.5"
- supports-color "^5.3.0"
-
chalk@~0.4.0:
version "0.4.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-0.4.0.tgz#5199a3ddcd0c1efe23bc08c1b027b06176e0c64f"
@@ -1775,14 +1757,10 @@ clone-stats@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/clone-stats/-/clone-stats-1.0.0.tgz#b3782dff8bb5474e18b9b6bf0fdfe782f8777680"
-clone@^1.0.0:
+clone@^1.0.0, clone@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.3.tgz#298d7e2231660f40c003c2ed3140decf3f53085f"
-clone@^1.0.2:
- version "1.0.2"
- resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.2.tgz#260b7a99ebb1edfe247538175f783243cb19d149"
-
clone@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/clone/-/clone-2.1.1.tgz#d217d1e961118e3ac9a4b8bba3285553bf647cdb"
@@ -2572,11 +2550,7 @@ di@^0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/di/-/di-0.0.1.tgz#806649326ceaa7caa3306d75d985ea2748ba913c"
-diff@^3.3.1, diff@^3.4.0:
- version "3.4.0"
- resolved "https://registry.yarnpkg.com/diff/-/diff-3.4.0.tgz#b1d85507daf3964828de54b37d0d73ba67dda56c"
-
-diff@^3.5.0:
+diff@^3.3.1, diff@^3.4.0, diff@^3.5.0:
version "3.5.0"
resolved "https://registry.yarnpkg.com/diff/-/diff-3.5.0.tgz#800c0dd1e0a8bfbc95835c202ad220fe317e5a12"
@@ -2716,11 +2690,7 @@ ee-first@1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
-ejs@^2.5.7:
- version "2.5.7"
- resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.7.tgz#cc872c168880ae3c7189762fd5ffc00896c9518a"
-
-ejs@^2.5.9:
+ejs@^2.5.7, ejs@^2.5.9:
version "2.5.9"
resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.9.tgz#7ba254582a560d267437109a68354112475b0ce5"
@@ -3319,14 +3289,6 @@ extend@3, extend@^3.0.0, extend@~3.0.0, extend@~3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
-external-editor@^2.0.4:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.1.0.tgz#3d026a21b7f95b5726387d4200ac160d372c3b48"
- dependencies:
- chardet "^0.4.0"
- iconv-lite "^0.4.17"
- tmp "^0.0.33"
-
external-editor@^2.1.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5"
@@ -3779,18 +3741,7 @@ glob@^5.0.15:
once "^1.3.0"
path-is-absolute "^1.0.0"
-glob@^7.0.0, glob@^7.0.3:
- version "7.1.1"
- resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
- dependencies:
- fs.realpath "^1.0.0"
- inflight "^1.0.4"
- inherits "2"
- minimatch "^3.0.2"
- once "^1.3.0"
- path-is-absolute "^1.0.0"
-
-glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
+glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
version "7.1.2"
resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
dependencies:
@@ -3865,7 +3816,7 @@ globby@^7.1.1:
pify "^3.0.0"
slash "^1.0.0"
-globby@^8.0.0:
+globby@^8.0.0, globby@^8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/globby/-/globby-8.0.1.tgz#b5ad48b8aa80b35b814fc1281ecc851f1d2b5b50"
dependencies:
@@ -4032,10 +3983,6 @@ has-flag@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa"
-has-flag@^2.0.0:
- version "2.0.0"
- resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51"
-
has-flag@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd"
@@ -4404,26 +4351,7 @@ inquirer@^0.12.0:
strip-ansi "^3.0.0"
through "^2.3.6"
-inquirer@^3.3.0:
- version "3.3.0"
- resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9"
- dependencies:
- ansi-escapes "^3.0.0"
- chalk "^2.0.0"
- cli-cursor "^2.1.0"
- cli-width "^2.0.0"
- external-editor "^2.0.4"
- figures "^2.0.0"
- lodash "^4.3.0"
- mute-stream "0.0.7"
- run-async "^2.2.0"
- rx-lite "^4.0.8"
- rx-lite-aggregates "^4.0.8"
- string-width "^2.1.0"
- strip-ansi "^4.0.0"
- through "^2.3.6"
-
-inquirer@^5.1.0:
+inquirer@^5.1.0, inquirer@^5.2.0:
version "5.2.0"
resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-5.2.0.tgz#db350c2b73daca77ff1243962e9f22f099685726"
dependencies:
@@ -4447,11 +4375,7 @@ internal-ip@1.2.0:
dependencies:
meow "^3.3.0"
-interpret@^1.0.0:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.0.1.tgz#d579fb7f693b858004947af39fa0db49f795602c"
-
-interpret@^1.0.4:
+interpret@^1.0.0, interpret@^1.0.4:
version "1.1.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
@@ -4868,7 +4792,7 @@ istanbul-lib-hook@^1.1.0:
dependencies:
append-transform "^0.4.0"
-istanbul-lib-instrument@^1.10.1:
+istanbul-lib-instrument@^1.10.1, istanbul-lib-instrument@^1.9.1:
version "1.10.1"
resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.10.1.tgz#724b4b6caceba8692d3f1f9d0727e279c401af7b"
dependencies:
@@ -4880,18 +4804,6 @@ istanbul-lib-instrument@^1.10.1:
istanbul-lib-coverage "^1.2.0"
semver "^5.3.0"
-istanbul-lib-instrument@^1.9.1:
- version "1.9.1"
- resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.9.1.tgz#250b30b3531e5d3251299fdd64b0b2c9db6b558e"
- dependencies:
- babel-generator "^6.18.0"
- babel-template "^6.16.0"
- babel-traverse "^6.18.0"
- babel-types "^6.18.0"
- babylon "^6.18.0"
- istanbul-lib-coverage "^1.1.1"
- semver "^5.3.0"
-
istanbul-lib-report@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.2.tgz#922be27c13b9511b979bd1587359f69798c1d425"
@@ -5455,11 +5367,7 @@ lodash@4.17.4:
version "4.17.4"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae"
-lodash@^4.0.0, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0:
- version "4.17.5"
- resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511"
-
-lodash@^4.17.10:
+lodash@^4.0.0, lodash@^4.11.1, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.2, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.0, lodash@^4.3.0, lodash@^4.5.0:
version "4.17.10"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7"
@@ -5469,13 +5377,7 @@ log-symbols@^1.0.2:
dependencies:
chalk "^1.0.0"
-log-symbols@^2.1.0:
- version "2.1.0"
- resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.1.0.tgz#f35fa60e278832b538dc4dddcbb478a45d3e3be6"
- dependencies:
- chalk "^2.0.1"
-
-log-symbols@^2.2.0:
+log-symbols@^2.1.0, log-symbols@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a"
dependencies:
@@ -5548,14 +5450,7 @@ lru-cache@2.2.x:
version "2.2.4"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.2.4.tgz#6c658619becf14031d0d0b594b16042ce4dc063d"
-lru-cache@^4.0.1, lru-cache@^4.1.1:
- version "4.1.1"
- resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.1.tgz#622e32e82488b49279114a4f9ecf45e7cd6bba55"
- dependencies:
- pseudomap "^1.0.2"
- yallist "^2.1.2"
-
-lru-cache@^4.1.2:
+lru-cache@^4.0.1, lru-cache@^4.1.1, lru-cache@^4.1.2:
version "4.1.3"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-4.1.3.tgz#a1175cf3496dfc8436c156c334b4955992bce69c"
dependencies:
@@ -5591,13 +5486,7 @@ mailgun-js@^0.7.0:
q "~1.4.0"
tsscmp "~1.0.0"
-make-dir@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.0.0.tgz#97a011751e91dd87cfadef58832ebb04936de978"
- dependencies:
- pify "^2.3.0"
-
-make-dir@^1.1.0:
+make-dir@^1.0.0, make-dir@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-1.2.0.tgz#6d6a49eead4aae296c53bbf3a1a008bd6c89469b"
dependencies:
@@ -5736,7 +5625,7 @@ micromatch@^2.1.5, micromatch@^2.3.7:
parse-glob "^3.0.4"
regex-cache "^0.4.2"
-micromatch@^3.1.10, micromatch@^3.1.9:
+micromatch@^3.1.10, micromatch@^3.1.4, micromatch@^3.1.8, micromatch@^3.1.9:
version "3.1.10"
resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23"
dependencies:
@@ -5754,42 +5643,6 @@ micromatch@^3.1.10, micromatch@^3.1.9:
snapdragon "^0.8.1"
to-regex "^3.0.2"
-micromatch@^3.1.4:
- version "3.1.6"
- resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.6.tgz#8d7c043b48156f408ca07a4715182b79b99420bf"
- dependencies:
- arr-diff "^4.0.0"
- array-unique "^0.3.2"
- braces "^2.3.1"
- define-property "^2.0.2"
- extend-shallow "^3.0.2"
- extglob "^2.0.4"
- fragment-cache "^0.2.1"
- kind-of "^6.0.2"
- nanomatch "^1.2.9"
- object.pick "^1.3.0"
- regex-not "^1.0.0"
- snapdragon "^0.8.1"
- to-regex "^3.0.1"
-
-micromatch@^3.1.8:
- version "3.1.9"
- resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.9.tgz#15dc93175ae39e52e93087847096effc73efcf89"
- dependencies:
- arr-diff "^4.0.0"
- array-unique "^0.3.2"
- braces "^2.3.1"
- define-property "^2.0.2"
- extend-shallow "^3.0.2"
- extglob "^2.0.4"
- fragment-cache "^0.2.1"
- kind-of "^6.0.2"
- nanomatch "^1.2.9"
- object.pick "^1.3.0"
- regex-not "^1.0.0"
- snapdragon "^0.8.1"
- to-regex "^3.0.1"
-
miller-rabin@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d"
@@ -5815,14 +5668,10 @@ mime@^1.3.4:
version "1.6.0"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
-mime@^2.0.3:
+mime@^2.0.3, mime@^2.1.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/mime/-/mime-2.3.1.tgz#b1621c54d63b97c47d3cfe7f7215f7d64517c369"
-mime@^2.1.0:
- version "2.2.0"
- resolved "https://registry.yarnpkg.com/mime/-/mime-2.2.0.tgz#161e541965551d3b549fa1114391e3a3d55b923b"
-
mimic-fn@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18"
@@ -6926,15 +6775,7 @@ postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0
source-map "^0.5.6"
supports-color "^3.2.3"
-postcss@^6.0.1, postcss@^6.0.14:
- version "6.0.19"
- resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.19.tgz#76a78386f670b9d9494a655bf23ac012effd1555"
- dependencies:
- chalk "^2.3.1"
- source-map "^0.6.1"
- supports-color "^5.2.0"
-
-postcss@^6.0.20:
+postcss@^6.0.1, postcss@^6.0.14, postcss@^6.0.20:
version "6.0.22"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.22.tgz#e23b78314905c3b90cbd61702121e7a78848f2a3"
dependencies:
@@ -6962,14 +6803,10 @@ prettier@1.11.1:
version "1.11.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.11.1.tgz#61e43fc4cd44e68f2b0dfc2c38cd4bb0fccdcc75"
-prettier@^1.11.1:
+prettier@^1.11.1, prettier@^1.5.3:
version "1.12.1"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.12.1.tgz#c1ad20e803e7749faf905a409d2367e06bbe7325"
-prettier@^1.5.3:
- version "1.10.2"
- resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.10.2.tgz#1af8356d1842276a99a5b5529c82dd9e9ad3cc93"
-
pretty-bytes@^4.0.2:
version "4.0.2"
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-4.0.2.tgz#b2bf82e7350d65c6c33aa95aaa5a4f6327f61cd9"
@@ -7615,18 +7452,12 @@ right-align@^0.1.1:
dependencies:
align-text "^0.1.1"
-rimraf@2, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2:
+rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.4, rimraf@^2.6.0, rimraf@^2.6.1, rimraf@^2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.2.tgz#2ed8150d24a16ea8651e6d6ef0f47c4158ce7a36"
dependencies:
glob "^7.0.5"
-rimraf@^2.2.8:
- version "2.6.1"
- resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d"
- dependencies:
- glob "^7.0.5"
-
rimraf@~2.2.6:
version "2.2.8"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.2.8.tgz#e439be2aaee327321952730f99a8929e4fc50582"
@@ -7656,16 +7487,6 @@ run-queue@^1.0.0, run-queue@^1.0.3:
dependencies:
aproba "^1.1.1"
-rx-lite-aggregates@^4.0.8:
- version "4.0.8"
- resolved "https://registry.yarnpkg.com/rx-lite-aggregates/-/rx-lite-aggregates-4.0.8.tgz#753b87a89a11c95467c4ac1626c4efc4e05c67be"
- dependencies:
- rx-lite "*"
-
-rx-lite@*, rx-lite@^4.0.8:
- version "4.0.8"
- resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-4.0.8.tgz#0b1e11af8bc44836f04a6407e92da42467b79444"
-
rx-lite@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102"
@@ -7737,11 +7558,7 @@ semver-diff@^2.0.0:
dependencies:
semver "^5.0.3"
-"semver@2 || 3 || 4 || 5", semver@^5.0.3:
- version "5.3.0"
- resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
-
-semver@^5.1.0, semver@^5.3.0, semver@^5.5.0:
+"semver@2 || 3 || 4 || 5", semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab"
@@ -8075,11 +7892,7 @@ source-map@^0.4.4:
dependencies:
amdefine ">=0.0.4"
-source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1:
- version "0.5.6"
- resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
-
-source-map@^0.5.7, source-map@~0.5.3, source-map@~0.5.6:
+source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.6:
version "0.5.7"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc"
@@ -8346,19 +8159,7 @@ supports-color@^3.1.0, supports-color@^3.1.2, supports-color@^3.2.3:
dependencies:
has-flag "^1.0.0"
-supports-color@^5.1.0:
- version "5.1.0"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.1.0.tgz#058a021d1b619f7ddf3980d712ea3590ce7de3d5"
- dependencies:
- has-flag "^2.0.0"
-
-supports-color@^5.2.0:
- version "5.2.0"
- resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.2.0.tgz#b0d5333b1184dd3666cbe5aa0b45c5ac7ac17a4a"
- dependencies:
- has-flag "^3.0.0"
-
-supports-color@^5.3.0, supports-color@^5.4.0:
+supports-color@^5.1.0, supports-color@^5.2.0, supports-color@^5.3.0, supports-color@^5.4.0:
version "5.4.0"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.4.0.tgz#1c6b337402c2137605efe19f10fec390f6faab54"
dependencies:
@@ -8555,15 +8356,7 @@ to-regex-range@^2.1.0:
is-number "^3.0.0"
repeat-string "^1.6.1"
-to-regex@^3.0.1:
- version "3.0.1"
- resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.1.tgz#15358bee4a2c83bd76377ba1dc049d0f18837aae"
- dependencies:
- define-property "^0.2.5"
- extend-shallow "^2.0.1"
- regex-not "^1.0.0"
-
-to-regex@^3.0.2:
+to-regex@^3.0.1, to-regex@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce"
dependencies:
@@ -9149,7 +8942,7 @@ webpack-dev-server@^3.1.4:
webpack-log "^1.1.2"
yargs "11.0.0"
-webpack-log@^1.0.1:
+webpack-log@^1.0.1, webpack-log@^1.1.2:
version "1.2.0"
resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-1.2.0.tgz#a4b34cda6b22b518dbb0ab32e567962d5c72a43d"
dependencies:
@@ -9158,23 +8951,7 @@ webpack-log@^1.0.1:
loglevelnext "^1.0.1"
uuid "^3.1.0"
-webpack-log@^1.1.2:
- version "1.1.2"
- resolved "https://registry.yarnpkg.com/webpack-log/-/webpack-log-1.1.2.tgz#cdc76016537eed24708dc6aa3d1e52189efee107"
- dependencies:
- chalk "^2.1.0"
- log-symbols "^2.1.0"
- loglevelnext "^1.0.1"
- uuid "^3.1.0"
-
-webpack-sources@^1.0.1:
- version "1.0.1"
- resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.0.1.tgz#c7356436a4d13123be2e2426a05d1dad9cbe65cf"
- dependencies:
- source-list-map "^2.0.0"
- source-map "~0.5.3"
-
-webpack-sources@^1.1.0:
+webpack-sources@^1.0.1, webpack-sources@^1.1.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/webpack-sources/-/webpack-sources-1.1.0.tgz#a101ebae59d6507354d71d8013950a3a8b7a5a54"
dependencies:
@@ -9415,39 +9192,23 @@ yeast@0.1.2:
version "0.1.2"
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
-yeoman-environment@^2.0.0:
- version "2.0.5"
- resolved "https://registry.yarnpkg.com/yeoman-environment/-/yeoman-environment-2.0.5.tgz#84f22bafa84088971fe99ea85f654a3a3dd2b693"
- dependencies:
- chalk "^2.1.0"
- debug "^3.1.0"
- diff "^3.3.1"
- escape-string-regexp "^1.0.2"
- globby "^6.1.0"
- grouped-queue "^0.3.3"
- inquirer "^3.3.0"
- is-scoped "^1.0.0"
- lodash "^4.17.4"
- log-symbols "^2.1.0"
- mem-fs "^1.1.0"
- text-table "^0.2.0"
- untildify "^3.0.2"
-
-yeoman-environment@^2.0.5:
- version "2.0.6"
- resolved "https://registry.yarnpkg.com/yeoman-environment/-/yeoman-environment-2.0.6.tgz#ae1b21d826b363f3d637f88a7fc9ea7414cb5377"
+yeoman-environment@^2.0.0, yeoman-environment@^2.0.5:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/yeoman-environment/-/yeoman-environment-2.1.1.tgz#10a045f7fc4397873764882eae055a33e56ee1c5"
dependencies:
chalk "^2.1.0"
+ cross-spawn "^6.0.5"
debug "^3.1.0"
diff "^3.3.1"
escape-string-regexp "^1.0.2"
- globby "^6.1.0"
+ globby "^8.0.1"
grouped-queue "^0.3.3"
- inquirer "^3.3.0"
+ inquirer "^5.2.0"
is-scoped "^1.0.0"
- lodash "^4.17.4"
+ lodash "^4.17.10"
log-symbols "^2.1.0"
mem-fs "^1.1.0"
+ strip-ansi "^4.0.0"
text-table "^0.2.0"
untildify "^3.0.2"