Commit 38bea69b authored by Michael Kozono's avatar Michael Kozono Committed by Nick Thomas

Add ability to set `alternate_url` in UI

Add the form field, and allow the data to flow through to the model
just like the regular `url` field.
parent 95178fa1
......@@ -1324,6 +1324,7 @@ ActiveRecord::Schema.define(version: 20190305162221) do
t.text "selective_sync_shards"
t.integer "verification_max_capacity", default: 100, null: false
t.integer "minimum_reverification_interval", default: 7, null: false
t.string "alternate_url"
t.index ["access_key"], name: "index_geo_nodes_on_access_key", using: :btree
t.index ["primary"], name: "index_geo_nodes_on_primary", using: :btree
t.index ["url"], name: "index_geo_nodes_on_url", unique: true, using: :btree
......
......@@ -31,6 +31,7 @@ Example response:
{
"id": 2,
"url": "https://secondary.example.com/",
"alternate_url": "https://alternate.example.com/",
"primary": false,
"enabled": true,
"current": false,
......@@ -83,6 +84,7 @@ PUT /geo_nodes/:id
| `id` | integer | yes | The ID of the Geo node. |
| `enabled` | boolean | no | Flag indicating if the Geo node is enabled. |
| `url` | string | no | The URL to connect to the Geo node. |
| `alternate_url` | string | no | Allows users to log in to the secondary at an alternate URL (required for OAuth) |
| `files_max_capacity` | integer | no | Control the maximum concurrency of LFS/attachment backfill for this secondary node. |
| `repos_max_capacity` | integer | no | Control the maximum concurrency of repository backfill for this secondary node. |
| `verification_max_capacity` | integer | no | Control the maximum concurrency of verification for this node. |
......@@ -93,6 +95,7 @@ Example response:
{
"id": 1,
"url": "https://secondary.example.com/",
"alternate_url": "https://alternate.example.com/",
"primary": false,
"enabled": true,
"current": true,
......
......@@ -27,6 +27,7 @@ Secondaries have a number of additional settings available:
| Selective synchronization | Enable Geo [selective sync](../../administration/geo/replication/configuration.md#selective-synchronization) for this secondary. |
| Repository sync capacity | Number of concurrent requests this secondary will make to the primary when backfilling repositories. |
| File sync capacity | Number of concurrent requests this secondary will make to the primary when backfilling files. |
| Alternate URL | Allows users to log in to the secondary at an alternate URL (required for OAuth) |
## Geo backfill
......@@ -45,3 +46,13 @@ the limits are configurable - if your primary node has lots of surplus capacity,
you can increase the values to complete backfill in a shorter time. If it's
under heavy load and backfill is reducing its availability for normal requests,
you can decrease them.
## Multiple secondaries behind a load balancer
Secondaries are authenticated via OAuth with the primary. For security, the
primary does not allow redirecting back to an arbitrary URL. If you want to
allow users to log in to secondaries at a common name/load balancer URL, then
this URL must be specified as the "Alternate URL" on every secondary behind it.
Additionally, the load balancer should use sticky sessions, since users must
authenticate each first request to each secondary.
......@@ -121,7 +121,7 @@ export default {
</script>
<template>
<div class="node-detail-item prepend-top-15 prepend-left-10">
<div class="node-detail-item">
<div class="node-detail-title">
<span> {{ itemTitle }} </span>
<icon
......
......@@ -74,7 +74,11 @@ export default {
:node-details="nodeDetails"
:node-type-primary="node.primary"
/>
<node-details-section-other :node-details="nodeDetails" :node-type-primary="node.primary" />
<node-details-section-other
:node="node"
:node-details="nodeDetails"
:node-type-primary="node.primary"
/>
<div v-if="hasError || hasVersionMismatch" class="node-health-message-container">
<p class="node-health-message">{{ errorMessage }}</p>
</div>
......
......@@ -17,6 +17,10 @@ export default {
},
mixins: [DetailsSectionMixin],
props: {
node: {
type: Object,
required: true,
},
nodeDetails: {
type: Object,
required: true,
......@@ -65,6 +69,12 @@ export default {
itemValueType: VALUE_TYPE.PLAIN,
cssClass: this.storageShardsCssClass,
},
{
itemTitle: s__('GeoNodes|Alternate URL'),
itemValue: this.node.alternateUrl,
itemValueType: VALUE_TYPE.PLAIN,
cssClass: 'node-detail-value-bold',
},
];
},
storageShardsStatus() {
......@@ -81,6 +91,14 @@ export default {
? `${cssClass} node-detail-value-error`
: cssClass;
},
sectionItemsContainerClasses() {
const { nodeTypePrimary, showSectionItems } = this;
return {
'col-md-6 prepend-left-15': nodeTypePrimary,
'row col-md-12 prepend-left-10': !nodeTypePrimary,
'd-flex': showSectionItems && !nodeTypePrimary,
};
},
},
methods: {
handleSectionToggle(toggleState) {
......@@ -100,15 +118,19 @@ export default {
</div>
<div
v-show="showSectionItems"
class="col-md-6 prepend-left-15 prepend-top-10 section-items-container"
:class="sectionItemsContainerClasses"
class="prepend-top-10 section-items-container"
>
<geo-node-detail-item
:item-title="s__('GeoNodes|Storage config')"
:item-value="storageShardsStatus"
:item-value-type="$options.valueType.PLAIN"
v-for="(nodeDetailItem, index) in nodeDetailItems"
:key="index"
:class="{ 'prepend-top-15 prepend-left-10': nodeTypePrimary, 'col-sm-3': !nodeTypePrimary }"
:css-class="nodeDetailItem.cssClass"
:item-title="nodeDetailItem.itemTitle"
:item-value="nodeDetailItem.itemValue"
:item-value-type="nodeDetailItem.itemValueType"
:item-value-stale="statusInfoStale"
:item-value-stale-tooltip="statusInfoStaleMessage"
:css-class="storageShardsCssClass"
/>
</div>
</div>
......
......@@ -141,6 +141,7 @@ export default {
:item-value-stale-tooltip="statusInfoStaleMessage"
:custom-type="nodeDetailItem.customType"
:event-type-log-status="nodeDetailItem.eventTypeLogStatus"
class="prepend-top-15 prepend-left-10"
/>
</div>
</div>
......
......@@ -136,6 +136,7 @@ export default {
:failure-label="nodeDetailItem.failureLabel"
:custom-type="nodeDetailItem.customType"
:help-info="nodeDetailItem.helpInfo"
class="prepend-top-15 prepend-left-10"
/>
</div>
</template>
......
......@@ -48,6 +48,7 @@ export default class GeoNodesStore {
primary,
current,
enabled,
alternateUrl: rawNode.alternate_url || '',
nodeActionActive: false,
basePath: rawNode._links.self,
repairPath: rawNode._links.repair,
......
......@@ -50,6 +50,7 @@ class Admin::Geo::NodesController < Admin::ApplicationController
def geo_node_params
params.require(:geo_node).permit(
:url,
:alternate_url,
:primary,
:selective_sync_type,
:namespace_ids,
......
......@@ -18,7 +18,8 @@ class GeoNode < ActiveRecord::Base
primary: false
validates :url, presence: true, uniqueness: { case_sensitive: false }
validate :check_url_is_valid
validate :url_is_http
validate :alternate_url_is_http
validates :primary, uniqueness: { message: 'node already exists' }, if: :primary
validates :enabled, if: :primary, acceptance: { message: 'Geo primary node cannot be disabled' }
......@@ -121,24 +122,33 @@ class GeoNode < ActiveRecord::Base
end
def url
value = read_attribute(:url)
value += '/' if value.present? && !value.end_with?('/')
value
read_with_ending_slash(:url)
end
def url=(value)
value += '/' if value.present? && !value.end_with?('/')
write_attribute(:url, value)
write_with_ending_slash(:url, value)
@uri = nil
end
def alternate_url
read_with_ending_slash(:alternate_url)
end
def alternate_url=(value)
write_with_ending_slash(:alternate_url, value)
@alternate_uri = nil
end
def uri
@uri ||= URI.parse(url) if url.present?
end
def alternate_uri
@alternate_uri ||= URI.parse(alternate_url) if alternate_url.present?
end
def geo_transfers_url(file_type, file_id)
geo_api_url("transfers/#{file_type}/#{file_id}")
end
......@@ -158,6 +168,12 @@ class GeoNode < ActiveRecord::Base
Gitlab::Routing.url_helpers.oauth_geo_callback_url(url_helper_args)
end
def alternate_oauth_callback_url
return unless alternate_url.present?
Gitlab::Routing.url_helpers.oauth_geo_callback_url(alternate_url_helper_args)
end
def oauth_logout_url(state)
Gitlab::Routing.url_helpers.oauth_geo_logout_url(url_helper_args.merge(state: state))
end
......@@ -257,7 +273,15 @@ class GeoNode < ActiveRecord::Base
end
def url_helper_args
{ protocol: uri.scheme, host: uri.host, port: uri.port, script_name: uri.path }
url_helper_options(uri)
end
def alternate_url_helper_args
url_helper_options(alternate_uri)
end
def url_helper_options(given_uri)
{ protocol: given_uri.scheme, host: given_uri.host, port: given_uri.port, script_name: given_uri.path }
end
def update_dependents_attributes
......@@ -276,12 +300,22 @@ class GeoNode < ActiveRecord::Base
end
end
def check_url_is_valid
if uri.present? && !%w[http https].include?(uri.scheme)
errors.add(:url, 'scheme must be http or https')
def url_is_http
url_is_http_for(:url, uri)
end
def alternate_url_is_http
url_is_http_for(:alternate_url, alternate_uri)
end
def url_is_http_for(attribute, uri_value)
return unless uri_value
unless %w[http https].include?(uri_value.scheme)
errors.add(attribute, 'scheme must be http or https')
end
rescue URI::InvalidURIError
errors.add(:url, 'is invalid')
errors.add(attribute, 'is not a valid URI')
end
def update_clone_url
......@@ -291,10 +325,29 @@ class GeoNode < ActiveRecord::Base
def update_oauth_application!
self.build_oauth_application if oauth_application.nil?
self.oauth_application.name = "Geo node: #{self.url}"
self.oauth_application.redirect_uri = oauth_callback_url
self.oauth_application.redirect_uri = [oauth_callback_url, alternate_oauth_callback_url].compact.join("\n")
end
def expire_cache!
Gitlab::Geo.expire_cache!
end
def read_with_ending_slash(attribute)
value = read_attribute(attribute)
add_ending_slash(value)
end
def write_with_ending_slash(attribute, value)
value = add_ending_slash(value)
write_attribute(attribute, value)
end
def add_ending_slash(value)
return value if value.blank?
return value if value.end_with?('/')
"#{value}/"
end
end
= form_errors(geo_node)
.form-group.row
.col-sm-2
= form.label :url, 'URL', class: 'col-form-label'
.col-sm-10
.form-row.form-group
.form-group.col-sm-6
= form.label :url, s_('Geo|URL'), class: 'font-weight-bold'
= form.text_field :url, class: 'form-control'
.form-group.row
.offset-sm-2.col-sm-10
.form-group.col-sm-6.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) }
= form.label :alternate_url, s_('Geo|Alternate URL'), class: 'font-weight-bold'
= form.text_field :alternate_url, class: 'form-control'
.form-text.text-muted= s_('Geo|To support OAuth logins to this node at a different domain than URL')
.form-row.form-group
.col-sm-12
.form-check
= form.check_box :primary, class: 'form-check-input'
= form.label :primary, class: 'form-check-label' do
%strong This is a primary node
.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) }
.form-group.row
.col-sm-2
= form.label :selective_sync_type, s_('Selective synchronization'), class: 'col-form-label'
.col-sm-10
= form.select :selective_sync_type, selective_sync_type_options_for_select(geo_node),
{}, { class: "form-control js-geo-node-selective-sync-type" }
.form-group.row.js-sync-by-namespace{ class: ('hidden' unless geo_node.selective_sync_by_namespaces?) }
.col-sm-2
= form.label :namespace_ids, s_('Geo|Groups to synchronize'), class: 'col-form-label'
.col-sm-10
= hidden_field_tag "#{form.object_name}[namespace_ids]", geo_node.namespace_ids.join(","), class: 'js-geo-node-namespaces', data: { selected: node_namespaces_options(geo_node.namespaces).to_json }
.form-text.text-muted
#{ s_("Choose which groups you wish to synchronize to this secondary node.") }
.form-group.row.js-sync-by-shard{ class: ('hidden' unless geo_node.selective_sync_by_shards?) }
.col-sm-2
= form.label :selective_sync_shards, s_('Geo|Shards to synchronize'), class: 'col-form-label'
.col-sm-10
= form.select :selective_sync_shards, repository_storages_options_for_select(geo_node.selective_sync_shards),
{ include_hidden: false }, multiple: true, class: 'form-control'
.form-text.text-muted
#{ s_("Choose which shards you wish to synchronize to this secondary node.") }
.form-group.row.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) }
.col-sm-2
= form.label :repos_max_capacity, s_('Geo|Repository sync capacity'), class: 'col-form-label'
.col-sm-10
= form.number_field :repos_max_capacity, class: 'form-control', min: 0
.form-text.text-muted
#{ s_('Control the maximum concurrency of repository backfill for this secondary node') }
.form-group.row.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) }
.col-sm-2
= form.label :files_max_capacity, s_('Geo|File sync capacity'), class: 'col-form-label'
.col-sm-10
= form.number_field :files_max_capacity, class: 'form-control', min: 0
.form-text.text-muted
#{ s_('Control the maximum concurrency of LFS/attachment backfill for this secondary node') }
.form-group.row
.col-sm-2
= form.label :verification_max_capacity, s_('Geo|Verification capacity'), class: 'col-form-label'
.col-sm-10
= form.number_field :verification_max_capacity, class: 'form-control', min: 0
.form-text.text-muted
#{ s_('Control the maximum concurrency of verification operations for this Geo node') }
.form-group.row.js-hide-if-geo-secondary{ class: ('hidden' unless geo_node.primary?) }
.col-sm-2
= form.label :minimum_reverification_interval, s_('Geo|Re-verification interval'), class: 'col-form-label'
.col-sm-10
= form.number_field :minimum_reverification_interval, class: 'form-control', min: 1
.form-text.text-muted
#{ s_('Control the minimum interval in days that a repository should be reverified for this primary node') }
%span= s_('Geo|This is a primary node')
.form-row.form-group.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) }
.col-sm-4
= form.label :selective_sync_type, s_('Geo|Selective synchronization'), class: 'font-weight-bold'
= form.select :selective_sync_type, selective_sync_type_options_for_select(geo_node), {}, { class: "form-control js-geo-node-selective-sync-type" }
.form-row.form-group.js-sync-by-namespace{ class: ('hidden' unless geo_node.selective_sync_by_namespaces?) }
.col-sm-4
= form.label :namespace_ids, s_('Geo|Groups to synchronize'), class: 'font-weight-bold'
= hidden_field_tag "#{form.object_name}[namespace_ids]", geo_node.namespace_ids.join(","), class: 'js-geo-node-namespaces', data: { selected: node_namespaces_options(geo_node.namespaces).to_json }
.form-text.text-muted= s_('Geo|Choose which groups you wish to synchronize to this secondary node.')
.form-row.form-group.js-sync-by-shard{ class: ('hidden' unless geo_node.selective_sync_by_shards?) }
.col-sm-4
= form.label :selective_sync_shards, s_('Geo|Shards to synchronize'), class: 'font-weight-bold'
= form.select :selective_sync_shards, repository_storages_options_for_select(geo_node.selective_sync_shards), { include_hidden: false }, multiple: true, class: 'form-control'
.form-text.text-muted= s_('Choose which shards you wish to synchronize to this secondary node.')
.form-row.form-group.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) }
.col-sm-8
= form.label :repos_max_capacity, s_('Geo|Repository sync capacity'), class: 'font-weight-bold'
= form.number_field :repos_max_capacity, class: 'form-control col-sm-2', min: 0
.form-text.text-muted= s_('Control the maximum concurrency of repository backfill for this secondary node')
.form-row.form-group.js-hide-if-geo-primary{ class: ('hidden' unless geo_node.secondary?) }
.col-sm-8
= form.label :files_max_capacity, s_('Geo|File sync capacity'), class: 'font-weight-bold'
= form.number_field :files_max_capacity, class: 'form-control col-sm-2', min: 0
.form-text.text-muted= s_('Geo|Control the maximum concurrency of LFS/attachment backfill for this secondary node')
.form-row.form-group
.col-sm-8
= form.label :verification_max_capacity, s_('Geo|Verification capacity'), class: 'font-weight-bold'
= form.number_field :verification_max_capacity, class: 'form-control col-sm-2', min: 0
.form-text.text-muted= s_('Geo|Control the maximum concurrency of verification operations for this Geo node')
.form-row.form-group.js-hide-if-geo-secondary{ class: ('hidden' unless geo_node.primary?) }
.col-sm-8
= form.label :minimum_reverification_interval, s_('Geo|Re-verification interval'), class: 'font-weight-bold'
= form.number_field :minimum_reverification_interval, class: 'form-control col-sm-2', min: 1
.form-text.text-muted= s_('Geo|Control the minimum interval in days that a repository should be reverified for this primary node')
---
title: 'Geo: Allow OAuth login to a secondary node at an alternate URL'
merge_request: 9544
author:
type: added
# frozen_string_literal: true
class AddAlternateUrlToGeoNodes < ActiveRecord::Migration[5.0]
def change
add_column :geo_nodes, :alternate_url, :string
end
end
......@@ -144,6 +144,7 @@ module API
params do
optional :enabled, type: Boolean, desc: 'Flag indicating if the Geo node is enabled'
optional :url, type: String, desc: 'The URL to connect to the Geo node'
optional :alternate_url, type: String, desc: 'An alternate URL to allow OAuth logins to this secondary node'
optional :files_max_capacity, type: Integer, desc: 'Control the maximum concurrency of LFS/attachment backfill for this secondary node'
optional :repos_max_capacity, type: Integer, desc: 'Control the maximum concurrency of repository backfill for this secondary node'
optional :verification_max_capacity, type: Integer, desc: 'Control the maximum concurrency of repository verification for this node'
......
......@@ -434,6 +434,7 @@ module EE
expose :id
expose :url
expose :alternate_url
expose :primary?, as: :primary
expose :enabled
expose :current?, as: :current
......
......@@ -110,7 +110,7 @@ describe Admin::Geo::NodesController, :postgresql do
end
describe '#update' do
let(:geo_node_attributes) { { url: 'http://example.com', selective_sync_shards: %w[foo bar] } }
let(:geo_node_attributes) { { url: 'http://example.com', alternate_url: 'http://anotherexample.com', selective_sync_shards: %w[foo bar] } }
let(:geo_node) { create(:geo_node) }
......@@ -137,6 +137,7 @@ describe Admin::Geo::NodesController, :postgresql do
geo_node.reload
expect(geo_node.url.chomp('/')).to eq(geo_node_attributes[:url])
expect(geo_node.alternate_url.chomp('/')).to eq(geo_node_attributes[:alternate_url])
expect(geo_node.selective_sync_shards).to eq(%w[foo bar])
end
......
......@@ -135,6 +135,20 @@ describe 'admin Geo Nodes', :js do
expect(page).to have_content('Primary')
end
end
it "allows the admin to update a secondary node's alternate URL" do
fill_in 'Alternate URL', with: 'http://someloadbalancer.com'
click_button 'Save changes'
expect(current_path).to eq admin_geo_nodes_path
wait_for_requests
page.within(find('.geo-node-item', match: :first)) do
click_button 'Other information'
expect(page).to have_content('http://someloadbalancer.com')
end
end
end
describe 'remove an existing Geo Node' do
......
......@@ -3,6 +3,7 @@
"required" : [
"id",
"url",
"alternate_url",
"primary",
"enabled",
"current",
......@@ -15,6 +16,7 @@
"properties" : {
"id": { "type": "integer" },
"url": { "type": ["string", "null"] },
"alternate_url": { "type": ["string", "null"] },
"primary": { "type": "boolean" },
"enabled": { "type": "boolean" },
"current": { "type": "boolean" },
......
......@@ -3,15 +3,17 @@ import Vue from 'vue';
import NodeDetailsSectionOtherComponent from 'ee/geo_nodes/components/node_detail_sections/node_details_section_other.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { mockNodeDetails } from 'ee_spec/geo_nodes/mock_data';
import { mockNode, mockNodeDetails } from 'ee_spec/geo_nodes/mock_data';
const createComponent = (
node = Object.assign({}, mockNode),
nodeDetails = Object.assign({}, mockNodeDetails),
nodeTypePrimary = false,
) => {
const Component = Vue.extend(NodeDetailsSectionOtherComponent);
return mountComponent(Component, {
node,
nodeDetails,
nodeTypePrimary,
});
......@@ -37,7 +39,7 @@ describe('NodeDetailsSectionOther', () => {
describe('computed', () => {
describe('nodeDetailItems', () => {
it('returns array containing items to show under primary node when prop `nodeTypePrimary` is true', () => {
const vmNodePrimary = createComponent(mockNodeDetails, true);
const vmNodePrimary = createComponent(mockNode, mockNodeDetails, true);
const items = vmNodePrimary.nodeDetailItems;
......@@ -53,7 +55,7 @@ describe('NodeDetailsSectionOther', () => {
it('returns array containing items to show under secondary node when prop `nodeTypePrimary` is false', () => {
const items = vm.nodeDetailItems;
expect(items.length).toBe(1);
expect(items.length).toBe(2);
expect(items[0].itemTitle).toBe('Storage config');
});
});
......
......@@ -25,6 +25,7 @@ export const mockNodes = [
{
id: 2,
url: 'http://127.0.0.1:3002/',
alternate_url: 'http://example.com:3002/',
primary: false,
enabled: true,
current: false,
......@@ -43,6 +44,7 @@ export const mockNodes = [
export const mockNode = {
id: 1,
url: 'http://127.0.0.1:3001/',
alternateUrl: '',
primary: true,
current: true,
enabled: true,
......
This diff is collapsed.
......@@ -212,6 +212,7 @@ describe API::GeoNodes, :geo, :prometheus, api: true do
params = {
enabled: false,
url: 'https://updated.example.com/',
alternate_url: 'https://alternate.example.com/',
files_max_capacity: 33,
repos_max_capacity: 44,
verification_max_capacity: 55
......
......@@ -1923,9 +1923,6 @@ msgstr ""
msgid "Choose what content you want to see on a group’s overview page"
msgstr ""
msgid "Choose which groups you wish to synchronize to this secondary node."
msgstr ""
msgid "Choose which repositories you want to connect and run CI/CD pipelines."
msgstr ""
......@@ -2888,18 +2885,9 @@ msgstr ""
msgid "Control the display of third party offers."
msgstr ""
msgid "Control the maximum concurrency of LFS/attachment backfill for this secondary node"
msgstr ""
msgid "Control the maximum concurrency of repository backfill for this secondary node"
msgstr ""
msgid "Control the maximum concurrency of verification operations for this Geo node"
msgstr ""
msgid "Control the minimum interval in days that a repository should be reverified for this primary node"
msgstr ""
msgid "ConvDev Index"
msgstr ""
......@@ -4572,6 +4560,9 @@ msgstr ""
msgid "GeoNodeSyncStatus|Node is slow, overloaded, or it just recovered after an outage."
msgstr ""
msgid "GeoNodes|Alternate URL"
msgstr ""
msgid "GeoNodes|Checksummed"
msgstr ""
......@@ -4752,9 +4743,24 @@ msgstr ""
msgid "Geo|All projects are being scheduled for re-sync"
msgstr ""
msgid "Geo|Alternate URL"
msgstr ""
msgid "Geo|Batch operations"
msgstr ""
msgid "Geo|Choose which groups you wish to synchronize to this secondary node."
msgstr ""
msgid "Geo|Control the maximum concurrency of LFS/attachment backfill for this secondary node"
msgstr ""
msgid "Geo|Control the maximum concurrency of verification operations for this Geo node"
msgstr ""
msgid "Geo|Control the minimum interval in days that a repository should be reverified for this primary node"
msgstr ""
msgid "Geo|Could not remove tracking entry for an existing project."
msgstr ""
......@@ -4842,6 +4848,9 @@ msgstr ""
msgid "Geo|Select groups to replicate."
msgstr ""
msgid "Geo|Selective synchronization"
msgstr ""
msgid "Geo|Shards to synchronize"
msgstr ""
......@@ -4854,12 +4863,21 @@ msgstr ""
msgid "Geo|Synchronization failed - %{error}"
msgstr ""
msgid "Geo|This is a primary node"
msgstr ""
msgid "Geo|To support OAuth logins to this node at a different domain than URL"
msgstr ""
msgid "Geo|Tracking entry for project (%{project_id}) was successfully removed."
msgstr ""
msgid "Geo|Tracking entry will be removed. Are you sure?"
msgstr ""
msgid "Geo|URL"
msgstr ""
msgid "Geo|Unknown state"
msgstr ""
......@@ -8900,9 +8918,6 @@ msgstr ""
msgid "Selecting a GitLab user will add a link to the GitLab user in the descriptions of issues and comments (e.g. \"By <a href=\"#\">@johnsmith</a>\"). It will also associate and/or assign these issues and comments with the selected user."
msgstr ""
msgid "Selective synchronization"
msgstr ""
msgid "Send email"
msgstr ""
......
......@@ -7,7 +7,7 @@ module QA
class New < QA::Page::Base
view 'ee/app/views/admin/geo/nodes/_form.html.haml' do
element :node_url_field, 'text_field :url' # rubocop:disable QA/ElementWithPattern
element :node_url_placeholder, "label :url, 'URL'" # rubocop:disable QA/ElementWithPattern
element :node_url_placeholder, "label :url" # rubocop:disable QA/ElementWithPattern
end
view 'ee/app/views/admin/geo/nodes/new.html.haml' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment