Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2019-10-22 18:06:06 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2019-10-22 18:06:06 +0300
commit6653aab95dfdfd260e8814e7499cc2345f451f99 (patch)
tree695acdeb5be70a87a26e39a592dd302f29d1c1fc /app
parentb1bcdba89bc241e2cede910f26cf3f5fff8d7901 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/cluster_app_logos/elastic_stack.pngbin0 -> 2919 bytes
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js2
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue80
-rw-r--r--app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue12
-rw-r--r--app/assets/javascripts/clusters/constants.js12
-rw-r--r--app/assets/javascripts/clusters/services/clusters_service.js1
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js33
-rw-r--r--app/assets/javascripts/pages/projects/project.js5
-rw-r--r--app/controllers/clusters/applications_controller.rb2
-rw-r--r--app/controllers/clusters/clusters_controller.rb3
-rw-r--r--app/models/clusters/applications/elastic_stack.rb79
-rw-r--r--app/models/clusters/applications/ingress.rb6
-rw-r--r--app/models/clusters/applications/runner.rb2
-rw-r--r--app/models/clusters/cluster.rb4
-rw-r--r--app/models/issue.rb11
-rw-r--r--app/serializers/cluster_application_entity.rb1
-rw-r--r--app/services/clusters/applications/base_service.rb6
-rw-r--r--app/views/clusters/clusters/show.html.haml1
-rw-r--r--app/views/projects/buttons/_clone.html.haml2
-rw-r--r--app/views/shared/_clone_panel.html.haml3
20 files changed, 241 insertions, 24 deletions
diff --git a/app/assets/images/cluster_app_logos/elastic_stack.png b/app/assets/images/cluster_app_logos/elastic_stack.png
new file mode 100644
index 00000000000..69fbc6aacd0
--- /dev/null
+++ b/app/assets/images/cluster_app_logos/elastic_stack.png
Binary files differ
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index 7ea8901ecbb..17251ccdffb 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -37,6 +37,7 @@ export default class Clusters {
installJupyterPath,
installKnativePath,
updateKnativePath,
+ installElasticStackPath,
installPrometheusPath,
managePrometheusPath,
clusterEnvironmentsPath,
@@ -86,6 +87,7 @@ export default class Clusters {
installJupyterEndpoint: installJupyterPath,
installKnativeEndpoint: installKnativePath,
updateKnativeEndpoint: updateKnativePath,
+ installElasticStackEndpoint: installElasticStackPath,
clusterEnvironmentsEndpoint: clusterEnvironmentsPath,
});
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index b95f97077f6..44d77277cc5 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -12,6 +12,7 @@ import certManagerLogo from 'images/cluster_app_logos/cert_manager.png';
import knativeLogo from 'images/cluster_app_logos/knative.png';
import meltanoLogo from 'images/cluster_app_logos/meltano.png';
import prometheusLogo from 'images/cluster_app_logos/prometheus.png';
+import elasticStackLogo from 'images/cluster_app_logos/elastic_stack.png';
import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
@@ -91,6 +92,7 @@ export default {
knativeLogo,
meltanoLogo,
prometheusLogo,
+ elasticStackLogo,
}),
computed: {
isProjectCluster() {
@@ -114,6 +116,9 @@ export default {
certManagerInstalled() {
return this.applications.cert_manager.status === APPLICATION_STATUS.INSTALLED;
},
+ enableClusterApplicationElasticStack() {
+ return gon.features && gon.features.enableClusterApplicationElasticStack;
+ },
ingressDescription() {
return sprintf(
_.escape(
@@ -168,6 +173,12 @@ export default {
jupyterHostname() {
return this.applications.jupyter.hostname;
},
+ elasticStackInstalled() {
+ return this.applications.elastic_stack.status === APPLICATION_STATUS.INSTALLED;
+ },
+ elasticStackKibanaHostname() {
+ return this.applications.elastic_stack.kibana_hostname;
+ },
knative() {
return this.applications.knative;
},
@@ -542,6 +553,75 @@ export default {
/>
</div>
</application-row>
+ <application-row
+ v-if="enableClusterApplicationElasticStack"
+ id="elastic_stack"
+ :logo-url="elasticStackLogo"
+ :title="applications.elastic_stack.title"
+ :status="applications.elastic_stack.status"
+ :status-reason="applications.elastic_stack.statusReason"
+ :request-status="applications.elastic_stack.requestStatus"
+ :request-reason="applications.elastic_stack.requestReason"
+ :version="applications.elastic_stack.version"
+ :chart-repo="applications.elastic_stack.chartRepo"
+ :update-available="applications.elastic_stack.updateAvailable"
+ :installed="applications.elastic_stack.installed"
+ :install-failed="applications.elastic_stack.installFailed"
+ :update-successful="applications.elastic_stack.updateSuccessful"
+ :update-failed="applications.elastic_stack.updateFailed"
+ :uninstallable="applications.elastic_stack.uninstallable"
+ :uninstall-successful="applications.elastic_stack.uninstallSuccessful"
+ :uninstall-failed="applications.elastic_stack.uninstallFailed"
+ :disabled="!helmInstalled"
+ :install-application-request-params="{
+ kibana_hostname: applications.elastic_stack.kibana_hostname,
+ }"
+ title-link="https://github.com/helm/charts/tree/master/stable/elastic-stack"
+ >
+ <div slot="description">
+ <p>
+ {{
+ s__(
+ `ClusterIntegration|The elastic stack collects logs from all pods in your cluster`,
+ )
+ }}
+ </p>
+
+ <template v-if="ingressExternalEndpoint">
+ <div class="form-group">
+ <label for="elastic-stack-kibana-hostname">{{
+ s__('ClusterIntegration|Kibana Hostname')
+ }}</label>
+
+ <div class="input-group">
+ <input
+ v-model="applications.elastic_stack.kibana_hostname"
+ :readonly="elasticStackInstalled"
+ type="text"
+ class="form-control js-hostname"
+ />
+ <span class="input-group-btn">
+ <clipboard-button
+ :text="elasticStackKibanaHostname"
+ :title="s__('ClusterIntegration|Copy Kibana Hostname')"
+ class="js-clipboard-btn"
+ />
+ </span>
+ </div>
+
+ <p v-if="ingressInstalled" class="form-text text-muted">
+ {{
+ s__(`ClusterIntegration|Replace this with your own hostname if you want.
+ If you do so, point hostname to Ingress IP Address from above.`)
+ }}
+ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
+ {{ __('More information') }}
+ </a>
+ </p>
+ </div>
+ </template>
+ </div>
+ </application-row>
</div>
</section>
</template>
diff --git a/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
index f1925c243f2..125bcaacc1c 100644
--- a/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
+++ b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
@@ -2,7 +2,16 @@
import { GlModal } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import trackUninstallButtonClickMixin from 'ee_else_ce/clusters/mixins/track_uninstall_button_click';
-import { HELM, INGRESS, CERT_MANAGER, PROMETHEUS, RUNNER, KNATIVE, JUPYTER } from '../constants';
+import {
+ HELM,
+ INGRESS,
+ CERT_MANAGER,
+ PROMETHEUS,
+ RUNNER,
+ KNATIVE,
+ JUPYTER,
+ ELASTIC_STACK,
+} from '../constants';
const CUSTOM_APP_WARNING_TEXT = {
[HELM]: sprintf(
@@ -28,6 +37,7 @@ const CUSTOM_APP_WARNING_TEXT = {
[JUPYTER]: s__(
'ClusterIntegration|All data not committed to GitLab will be deleted and cannot be restored.',
),
+ [ELASTIC_STACK]: s__('ClusterIntegration|All data will be deleted and cannot be restored.'),
};
export default {
diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js
index c6e4b7951cf..d7152e32376 100644
--- a/app/assets/javascripts/clusters/constants.js
+++ b/app/assets/javascripts/clusters/constants.js
@@ -51,7 +51,17 @@ export const KNATIVE = 'knative';
export const RUNNER = 'runner';
export const CERT_MANAGER = 'cert_manager';
export const PROMETHEUS = 'prometheus';
+export const ELASTIC_STACK = 'elastic_stack';
-export const APPLICATIONS = [HELM, INGRESS, JUPYTER, KNATIVE, RUNNER, CERT_MANAGER, PROMETHEUS];
+export const APPLICATIONS = [
+ HELM,
+ INGRESS,
+ JUPYTER,
+ KNATIVE,
+ RUNNER,
+ CERT_MANAGER,
+ PROMETHEUS,
+ ELASTIC_STACK,
+];
export const INGRESS_DOMAIN_SUFFIX = '.nip.io';
diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js
index fa12802b3de..dab4da04bf3 100644
--- a/app/assets/javascripts/clusters/services/clusters_service.js
+++ b/app/assets/javascripts/clusters/services/clusters_service.js
@@ -11,6 +11,7 @@ export default class ClusterService {
prometheus: this.options.installPrometheusEndpoint,
jupyter: this.options.installJupyterEndpoint,
knative: this.options.installKnativeEndpoint,
+ elastic_stack: this.options.installElasticStackEndpoint,
};
this.appUpdateEndpointMap = {
knative: this.options.updateKnativeEndpoint,
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index 6464461ea0c..6304e81c296 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -5,6 +5,7 @@ import {
JUPYTER,
KNATIVE,
CERT_MANAGER,
+ ELASTIC_STACK,
RUNNER,
APPLICATION_INSTALLED_STATUSES,
APPLICATION_STATUS,
@@ -85,6 +86,11 @@ export default class ClusterStore {
updateSuccessful: false,
updateFailed: false,
},
+ elastic_stack: {
+ ...applicationInitialState,
+ title: s__('ClusterIntegration|Elastic Stack'),
+ kibana_hostname: null,
+ },
},
environments: [],
fetchingEnvironments: false,
@@ -198,12 +204,11 @@ export default class ClusterStore {
this.state.applications.cert_manager.email =
this.state.applications.cert_manager.email || serverAppEntry.email;
} else if (appId === JUPYTER) {
- this.state.applications.jupyter.hostname =
- this.state.applications.jupyter.hostname ||
- serverAppEntry.hostname ||
- (this.state.applications.ingress.externalIp
- ? `jupyter.${this.state.applications.ingress.externalIp}.nip.io`
- : '');
+ this.state.applications.jupyter.hostname = this.updateHostnameIfUnset(
+ this.state.applications.jupyter.hostname,
+ serverAppEntry.hostname,
+ 'jupyter',
+ );
} else if (appId === KNATIVE) {
if (!this.state.applications.knative.isEditingHostName) {
this.state.applications.knative.hostname =
@@ -216,10 +221,26 @@ export default class ClusterStore {
} else if (appId === RUNNER) {
this.state.applications.runner.version = version;
this.state.applications.runner.updateAvailable = updateAvailable;
+ } else if (appId === ELASTIC_STACK) {
+ this.state.applications.elastic_stack.kibana_hostname = this.updateHostnameIfUnset(
+ this.state.applications.elastic_stack.kibana_hostname,
+ serverAppEntry.kibana_hostname,
+ 'kibana',
+ );
}
});
}
+ updateHostnameIfUnset(current, updated, fallback) {
+ return (
+ current ||
+ updated ||
+ (this.state.applications.ingress.externalIp
+ ? `${fallback}.${this.state.applications.ingress.externalIp}.nip.io`
+ : '')
+ );
+ }
+
toggleFetchEnvironments(isFetching) {
this.state.fetchingEnvironments = isFetching;
}
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index 435e8705803..01acfca158f 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -40,11 +40,6 @@ export default class Project {
$label.text(activeText);
});
- $('#modal-geo-info').data({
- cloneUrlSecondary: $this.attr('href'),
- cloneUrlPrimary: $this.data('primaryUrl') || '',
- });
-
if (mobileCloneField) {
mobileCloneField.dataset.clipboardText = url;
} else {
diff --git a/app/controllers/clusters/applications_controller.rb b/app/controllers/clusters/applications_controller.rb
index 16c2365f85d..5364116a5f8 100644
--- a/app/controllers/clusters/applications_controller.rb
+++ b/app/controllers/clusters/applications_controller.rb
@@ -47,7 +47,7 @@ class Clusters::ApplicationsController < Clusters::BaseController
end
def cluster_application_params
- params.permit(:application, :hostname, :email)
+ params.permit(:application, :hostname, :kibana_hostname, :email)
end
def cluster_application_destroy_params
diff --git a/app/controllers/clusters/clusters_controller.rb b/app/controllers/clusters/clusters_controller.rb
index 993aba661f3..bb47ccb72b3 100644
--- a/app/controllers/clusters/clusters_controller.rb
+++ b/app/controllers/clusters/clusters_controller.rb
@@ -15,6 +15,9 @@ class Clusters::ClustersController < Clusters::BaseController
before_action only: [:new, :create_gcp] do
push_frontend_feature_flag(:create_eks_clusters)
end
+ before_action only: [:show] do
+ push_frontend_feature_flag(:enable_cluster_application_elastic_stack)
+ end
helper_method :token_in_session
diff --git a/app/models/clusters/applications/elastic_stack.rb b/app/models/clusters/applications/elastic_stack.rb
new file mode 100644
index 00000000000..addf092fc5e
--- /dev/null
+++ b/app/models/clusters/applications/elastic_stack.rb
@@ -0,0 +1,79 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Applications
+ class ElasticStack < ApplicationRecord
+ VERSION = '1.8.0'
+
+ self.table_name = 'clusters_applications_elastic_stacks'
+
+ include ::Clusters::Concerns::ApplicationCore
+ include ::Clusters::Concerns::ApplicationStatus
+ include ::Clusters::Concerns::ApplicationVersion
+ include ::Clusters::Concerns::ApplicationData
+
+ default_value_for :version, VERSION
+
+ def set_initial_status
+ return unless not_installable?
+ return unless cluster&.application_ingress_available?
+
+ ingress = cluster.application_ingress
+ self.status = status_states[:installable] if ingress.external_ip_or_hostname?
+ end
+
+ def chart
+ 'stable/elastic-stack'
+ end
+
+ def values
+ content_values.to_yaml
+ end
+
+ def install_command
+ Gitlab::Kubernetes::Helm::InstallCommand.new(
+ name: 'elastic-stack',
+ version: VERSION,
+ rbac: cluster.platform_kubernetes_rbac?,
+ chart: chart,
+ files: files
+ )
+ end
+
+ def uninstall_command
+ Gitlab::Kubernetes::Helm::DeleteCommand.new(
+ name: 'elastic-stack',
+ rbac: cluster.platform_kubernetes_rbac?,
+ files: files,
+ postdelete: post_delete_script
+ )
+ end
+
+ private
+
+ def specification
+ {
+ "kibana" => {
+ "ingress" => {
+ "hosts" => [kibana_hostname],
+ "tls" => [{
+ "hosts" => [kibana_hostname],
+ "secretName" => "kibana-cert"
+ }]
+ }
+ }
+ }
+ end
+
+ def content_values
+ YAML.load_file(chart_values_file).deep_merge!(specification)
+ end
+
+ def post_delete_script
+ [
+ Gitlab::Kubernetes::KubectlCmd.delete("pvc", "--selector", "release=elastic-stack")
+ ].compact
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb
index 885e4ff7197..48712c8cc09 100644
--- a/app/models/clusters/applications/ingress.rb
+++ b/app/models/clusters/applications/ingress.rb
@@ -40,7 +40,7 @@ module Clusters
end
def allowed_to_uninstall?
- external_ip_or_hostname? && application_jupyter_nil_or_installable?
+ external_ip_or_hostname? && application_jupyter_nil_or_installable? && application_elastic_stack_nil_or_installable?
end
def install_command
@@ -91,6 +91,10 @@ module Clusters
def application_jupyter_nil_or_installable?
cluster.application_jupyter.nil? || cluster.application_jupyter&.installable?
end
+
+ def application_elastic_stack_nil_or_installable?
+ cluster.application_elastic_stack.nil? || cluster.application_elastic_stack&.installable?
+ end
end
end
end
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index 954046c143b..fa15ec6703b 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -3,7 +3,7 @@
module Clusters
module Applications
class Runner < ApplicationRecord
- VERSION = '0.9.0'
+ VERSION = '0.10.0'
self.table_name = 'clusters_applications_runners'
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index d6f5d7c3f93..c600717f5df 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -18,7 +18,8 @@ module Clusters
Applications::Prometheus.application_name => Applications::Prometheus,
Applications::Runner.application_name => Applications::Runner,
Applications::Jupyter.application_name => Applications::Jupyter,
- Applications::Knative.application_name => Applications::Knative
+ Applications::Knative.application_name => Applications::Knative,
+ Applications::ElasticStack.application_name => Applications::ElasticStack
}.merge(PROJECT_ONLY_APPLICATIONS).freeze
DEFAULT_ENVIRONMENT = '*'
KUBE_INGRESS_BASE_DOMAIN = 'KUBE_INGRESS_BASE_DOMAIN'
@@ -51,6 +52,7 @@ module Clusters
has_one_cluster_application :runner
has_one_cluster_application :jupyter
has_one_cluster_application :knative
+ has_one_cluster_application :elastic_stack
has_many :kubernetes_namespaces
diff --git a/app/models/issue.rb b/app/models/issue.rb
index b9b481ac29b..91bef81227f 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -206,7 +206,16 @@ class Issue < ApplicationRecord
if self.confidential?
"#{iid}-confidential-issue"
else
- "#{iid}-#{title.parameterize}"
+ branch_name = "#{iid}-#{title.parameterize}"
+
+ if branch_name.length > 100
+ truncated_string = branch_name[0, 100]
+ # Delete everything dangling after the last hyphen so as not to risk
+ # existence of unintended words in the branch name due to mid-word split.
+ branch_name = truncated_string[0, truncated_string.rindex("-")]
+ end
+
+ branch_name
end
end
diff --git a/app/serializers/cluster_application_entity.rb b/app/serializers/cluster_application_entity.rb
index 2a916b13f52..a5b983d4074 100644
--- a/app/serializers/cluster_application_entity.rb
+++ b/app/serializers/cluster_application_entity.rb
@@ -8,6 +8,7 @@ class ClusterApplicationEntity < Grape::Entity
expose :external_ip, if: -> (e, _) { e.respond_to?(:external_ip) }
expose :external_hostname, if: -> (e, _) { e.respond_to?(:external_hostname) }
expose :hostname, if: -> (e, _) { e.respond_to?(:hostname) }
+ expose :kibana_hostname, if: -> (e, _) { e.respond_to?(:kibana_hostname) }
expose :email, if: -> (e, _) { e.respond_to?(:email) }
expose :update_available?, as: :update_available, if: -> (e, _) { e.respond_to?(:update_available?) }
expose :can_uninstall?, as: :can_uninstall
diff --git a/app/services/clusters/applications/base_service.rb b/app/services/clusters/applications/base_service.rb
index 67fb3ac8355..b39fc7d20a9 100644
--- a/app/services/clusters/applications/base_service.rb
+++ b/app/services/clusters/applications/base_service.rb
@@ -19,6 +19,10 @@ module Clusters
application.hostname = params[:hostname]
end
+ if application.has_attribute?(:kibana_hostname)
+ application.kibana_hostname = params[:kibana_hostname]
+ end
+
if application.has_attribute?(:email)
application.email = params[:email]
end
@@ -60,7 +64,7 @@ module Clusters
end
def invalid_application?
- unknown_application? || (!cluster.project_type? && project_only_application?)
+ unknown_application? || (!cluster.project_type? && project_only_application?) || (application_name == Applications::ElasticStack.application_name && !Feature.enabled?(:enable_cluster_application_elastic_stack))
end
def unknown_application?
diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml
index 31d5f592d75..c5288c8d02d 100644
--- a/app/views/clusters/clusters/show.html.haml
+++ b/app/views/clusters/clusters/show.html.haml
@@ -17,6 +17,7 @@
install_jupyter_path: clusterable.install_applications_cluster_path(@cluster, :jupyter),
install_knative_path: clusterable.install_applications_cluster_path(@cluster, :knative),
update_knative_path: clusterable.update_applications_cluster_path(@cluster, :knative),
+ install_elastic_stack_path: clusterable.install_applications_cluster_path(@cluster, :elastic_stack),
cluster_environments_path: cluster_environments_path,
toggle_status: @cluster.enabled? ? 'true': 'false',
has_rbac: has_rbac_enabled?(@cluster) ? 'true': 'false',
diff --git a/app/views/projects/buttons/_clone.html.haml b/app/views/projects/buttons/_clone.html.haml
index abef33ca01c..ed22573b23e 100644
--- a/app/views/projects/buttons/_clone.html.haml
+++ b/app/views/projects/buttons/_clone.html.haml
@@ -25,5 +25,3 @@
= clipboard_button(target: '#http_project_clone', title: _("Copy URL"), class: "input-group-text btn-default btn-clipboard")
= render_if_exists 'projects/buttons/geo'
= render_if_exists 'projects/buttons/kerberos_clone_field'
-
-= render_if_exists 'shared/geo_info_modal', project: project
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index cb834878276..3e805189055 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -22,6 +22,3 @@
.input-group-append
= clipboard_button(target: '#project_clone', title: _("Copy URL"), class: "input-group-text btn-default btn-clipboard")
- = render_if_exists 'shared/geo_modal_button'
-
-= render_if_exists 'shared/geo_modal', project: project