Commit 8bb85704 authored by Stan Hu's avatar Stan Hu

Merge branch '118841_03-node-form-selective-sync-shards' into 'master'

Geo Node Form in Vue - Selective Sync (Shards)

Closes #118841

See merge request gitlab-org/gitlab!24015
parents e7f9dff2 34d2ee0f
...@@ -119,6 +119,17 @@ module ApplicationSettingsHelper ...@@ -119,6 +119,17 @@ module ApplicationSettingsHelper
options_for_select(options, selected) options_for_select(options, selected)
end end
def repository_storages_options_json
options = Gitlab.config.repositories.storages.map do |name, storage|
{
label: "#{name} - #{storage['gitaly_address']}",
value: name
}
end
options.to_json
end
def external_authorization_description def external_authorization_description
_("If enabled, access to projects will be validated on an external service"\ _("If enabled, access to projects will be validated on an external service"\
" using their classification label.") " using their classification label.")
......
...@@ -7,6 +7,14 @@ export default { ...@@ -7,6 +7,14 @@ export default {
GeoNodeForm, GeoNodeForm,
}, },
props: { props: {
selectiveSyncTypes: {
type: Object,
required: true,
},
syncShardsOptions: {
type: Array,
required: true,
},
node: { node: {
type: Object, type: Object,
required: false, required: false,
...@@ -19,6 +27,6 @@ export default { ...@@ -19,6 +27,6 @@ export default {
<template> <template>
<article class="geo-node-form-container"> <article class="geo-node-form-container">
<h3 class="page-title">{{ node ? __('Edit Geo Node') : __('New Geo Node') }}</h3> <h3 class="page-title">{{ node ? __('Edit Geo Node') : __('New Geo Node') }}</h3>
<geo-node-form :node="node" /> <geo-node-form v-bind="$props" />
</article> </article>
</template> </template>
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
import { GlFormGroup, GlFormInput, GlFormCheckbox, GlButton } from '@gitlab/ui'; import { GlFormGroup, GlFormInput, GlFormCheckbox, GlButton } from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import GeoNodeFormCore from './geo_node_form_core.vue'; import GeoNodeFormCore from './geo_node_form_core.vue';
import GeoNodeFormSelectiveSync from './geo_node_form_selective_sync.vue';
import GeoNodeFormCapacities from './geo_node_form_capacities.vue'; import GeoNodeFormCapacities from './geo_node_form_capacities.vue';
export default { export default {
...@@ -12,6 +13,7 @@ export default { ...@@ -12,6 +13,7 @@ export default {
GlFormCheckbox, GlFormCheckbox,
GlButton, GlButton,
GeoNodeFormCore, GeoNodeFormCore,
GeoNodeFormSelectiveSync,
GeoNodeFormCapacities, GeoNodeFormCapacities,
}, },
props: { props: {
...@@ -20,6 +22,14 @@ export default { ...@@ -20,6 +22,14 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
selectiveSyncTypes: {
type: Object,
required: true,
},
syncShardsOptions: {
type: Array,
required: true,
},
}, },
data() { data() {
return { return {
...@@ -49,6 +59,12 @@ export default { ...@@ -49,6 +59,12 @@ export default {
redirect() { redirect() {
visitUrl('/admin/geo/nodes'); visitUrl('/admin/geo/nodes');
}, },
addSyncOption({ key, value }) {
this.nodeData[key].push(value);
},
removeSyncOption({ key, index }) {
this.nodeData[key].splice(index, 1);
},
}, },
}; };
</script> </script>
...@@ -74,6 +90,14 @@ export default { ...@@ -74,6 +90,14 @@ export default {
> >
<gl-form-input id="node-internal-url-field" v-model="nodeData.internalUrl" type="text" /> <gl-form-input id="node-internal-url-field" v-model="nodeData.internalUrl" type="text" />
</gl-form-group> </gl-form-group>
<geo-node-form-selective-sync
v-if="!nodeData.primary"
:node-data="nodeData"
:selective-sync-types="selectiveSyncTypes"
:sync-shards-options="syncShardsOptions"
@addSyncOption="addSyncOption"
@removeSyncOption="removeSyncOption"
/>
<geo-node-form-capacities :node-data="nodeData" /> <geo-node-form-capacities :node-data="nodeData" />
<gl-form-group <gl-form-group
v-if="!nodeData.primary" v-if="!nodeData.primary"
......
<script>
import { GlFormGroup, GlFormSelect } from '@gitlab/ui';
import GeoNodeFormShards from './geo_node_form_shards.vue';
export default {
name: 'GeoNodeFormSelectiveSync',
components: {
GlFormGroup,
GlFormSelect,
GeoNodeFormShards,
},
props: {
nodeData: {
type: Object,
required: true,
},
selectiveSyncTypes: {
type: Object,
required: true,
},
syncShardsOptions: {
type: Array,
required: true,
},
},
computed: {
selectiveSyncShards() {
return this.nodeData.selectiveSyncType === this.selectiveSyncTypes.SHARDS.value;
},
},
methods: {
addSyncOption({ key, value }) {
this.$emit('addSyncOption', { key, value });
},
removeSyncOption({ key, index }) {
this.$emit('removeSyncOption', { key, index });
},
},
};
</script>
<template>
<div ref="geoNodeFormSelectiveSyncContainer">
<gl-form-group
:label="__('Selective synchronization')"
label-for="node-selective-synchronization-field"
>
<gl-form-select
id="node-selective-synchronization-field"
v-model="nodeData.selectiveSyncType"
:options="selectiveSyncTypes"
value-field="value"
text-field="label"
class="col-sm-6"
/>
</gl-form-group>
<gl-form-group
v-if="selectiveSyncShards"
:label="__('Shards to synchronize')"
label-for="node-synchronization-shards-field"
:description="__('Choose which shards you wish to synchronize to this secondary node')"
>
<geo-node-form-shards
id="node-synchronization-shards-field"
:selected-shards="nodeData.selectiveSyncShards"
:sync-shards-options="syncShardsOptions"
@addSyncOption="addSyncOption"
@removeSyncOption="removeSyncOption"
/>
</gl-form-group>
</div>
</template>
<script>
import { GlIcon, GlDropdown, GlButton } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import { SELECTIVE_SYNC_SHARDS } from '../constants';
export default {
name: 'GeoNodeFormShards',
components: {
GlIcon,
GlDropdown,
GlButton,
},
props: {
syncShardsOptions: {
type: Array,
required: true,
},
selectedShards: {
type: Array,
required: true,
},
},
computed: {
dropdownTitle() {
if (this.selectedShards.length === 0) {
return __('Select shards to replicate');
}
return sprintf(__('Shards selected: %{count}'), { count: this.selectedShards.length });
},
noSyncShards() {
return this.syncShardsOptions.length === 0;
},
},
methods: {
toggleShard(shard) {
const index = this.selectedShards.findIndex(value => value === shard.value);
if (index > -1) {
this.$emit('removeSyncOption', { key: SELECTIVE_SYNC_SHARDS, index });
} else {
this.$emit('addSyncOption', { key: SELECTIVE_SYNC_SHARDS, value: shard.value });
}
},
isSelected(shard) {
return this.selectedShards.includes(shard.value);
},
},
};
</script>
<template>
<gl-dropdown :text="dropdownTitle">
<li v-for="shard in syncShardsOptions" :key="shard.value">
<gl-button class="d-flex align-items-center" @click="toggleShard(shard)">
<gl-icon :class="[{ invisible: !isSelected(shard) }]" name="mobile-issue-close" />
<span class="ml-1">{{ shard.label }}</span>
</gl-button>
</li>
<div v-if="noSyncShards" class="text-secondary p-2">{{ __('Nothing found…') }}</div>
</gl-dropdown>
</template>
/* eslint-disable import/prefer-default-export */
export const SELECTIVE_SYNC_SHARDS = 'selectiveSyncShards';
...@@ -15,7 +15,7 @@ export default () => { ...@@ -15,7 +15,7 @@ export default () => {
}, },
render(createElement) { render(createElement) {
const { const {
dataset: { nodeData }, dataset: { selectiveSyncTypes, syncShardsOptions, nodeData },
} = this.$options.el; } = this.$options.el;
let node; let node;
...@@ -26,6 +26,8 @@ export default () => { ...@@ -26,6 +26,8 @@ export default () => {
return createElement('geo-node-form-app', { return createElement('geo-node-form-app', {
props: { props: {
selectiveSyncTypes: JSON.parse(selectiveSyncTypes),
syncShardsOptions: JSON.parse(syncShardsOptions),
node, node,
}, },
}); });
......
...@@ -65,6 +65,25 @@ module EE ...@@ -65,6 +65,25 @@ module EE
) )
end end
def selective_sync_types_json
options = {
ALL: {
label: s_('Geo|All projects'),
value: ''
},
NAMESPACES: {
label: s_('Geo|Projects in certain groups'),
value: 'namespaces'
},
SHARDS: {
label: s_('Geo|Projects in certain storage shards'),
value: 'shards'
}
}
options.to_json
end
def status_loading_icon def status_loading_icon
icon "spinner spin fw", class: 'js-geo-node-loading' icon "spinner spin fw", class: 'js-geo-node-loading'
end end
......
- page_title _('Edit Geo Node') - page_title _('Edit Geo Node')
- if Feature.enabled?(:enable_geo_node_form_js) - if Feature.enabled?(:enable_geo_node_form_js)
#js-geo-node-form{ data: { "node-data" => @serialized_node } } #js-geo-node-form{ data: { "selective-sync-types" => selective_sync_types_json,
"sync-shards-options" => repository_storages_options_json,
"node-data" => @serialized_node } }
- else - else
%h3.page-title %h3.page-title
Edit Geo Node Edit Geo Node
......
- page_title _('New Geo Node') - page_title _('New Geo Node')
- if Feature.enabled?(:enable_geo_node_form_js) - if Feature.enabled?(:enable_geo_node_form_js)
#js-geo-node-form #js-geo-node-form{ data: { "selective-sync-types" => selective_sync_types_json,
"sync-shards-options" => repository_storages_options_json } }
- else - else
%h2.page-title %h2.page-title
%span.title-text %span.title-text
......
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import GeoNodeFormApp from 'ee/geo_node_form/components/app.vue'; import GeoNodeFormApp from 'ee/geo_node_form/components/app.vue';
import GeoNodeForm from 'ee/geo_node_form/components/geo_node_form.vue'; import GeoNodeForm from 'ee/geo_node_form/components/geo_node_form.vue';
import { MOCK_NODE } from '../mock_data'; import { MOCK_SELECTIVE_SYNC_TYPES, MOCK_SYNC_SHARDS, MOCK_NODE } from '../mock_data';
describe('GeoNodeFormApp', () => { describe('GeoNodeFormApp', () => {
let wrapper; let wrapper;
const propsData = { const propsData = {
selectiveSyncTypes: MOCK_SELECTIVE_SYNC_TYPES,
syncShardsOptions: MOCK_SYNC_SHARDS,
node: undefined, node: undefined,
}; };
......
import { mount } from '@vue/test-utils';
import GeoNodeFormSelectiveSync from 'ee/geo_node_form/components/geo_node_form_selective_sync.vue';
import GeoNodeFormShards from 'ee/geo_node_form/components/geo_node_form_shards.vue';
import { MOCK_NODE, MOCK_SELECTIVE_SYNC_TYPES, MOCK_SYNC_SHARDS } from '../mock_data';
describe('GeoNodeFormSelectiveSync', () => {
let wrapper;
const propsData = {
nodeData: MOCK_NODE,
selectiveSyncTypes: MOCK_SELECTIVE_SYNC_TYPES,
syncShardsOptions: MOCK_SYNC_SHARDS,
};
const createComponent = () => {
wrapper = mount(GeoNodeFormSelectiveSync, {
propsData,
});
};
afterEach(() => {
wrapper.destroy();
});
const findGeoNodeFormSyncContainer = () =>
wrapper.find({ ref: 'geoNodeFormSelectiveSyncContainer' });
const findGeoNodeFormSyncTypeField = () => wrapper.find('#node-selective-synchronization-field');
const findGeoNodeFormShardsField = () => wrapper.find(GeoNodeFormShards);
describe('template', () => {
beforeEach(() => {
createComponent();
});
it('renders Geo Node Form Sync Container', () => {
expect(findGeoNodeFormSyncContainer().exists()).toBe(true);
});
it('renders Geo Node Sync Type Field', () => {
expect(findGeoNodeFormSyncTypeField().exists()).toBe(true);
});
describe.each`
syncType | showShards
${MOCK_SELECTIVE_SYNC_TYPES.ALL} | ${false}
${MOCK_SELECTIVE_SYNC_TYPES.NAMESPACES} | ${false}
${MOCK_SELECTIVE_SYNC_TYPES.SHARDS} | ${true}
`(`sync type`, ({ syncType, showShards }) => {
beforeEach(() => {
createComponent();
wrapper.setProps({
nodeData: { ...propsData.nodeData, selectiveSyncType: syncType.value },
});
});
it(`${showShards ? 'show' : 'hide'} Shards Field`, () => {
expect(findGeoNodeFormShardsField().exists()).toBe(showShards);
});
});
});
describe('methods', () => {
describe('addSyncOption', () => {
beforeEach(() => {
createComponent();
});
it('emits `addSyncOption`', () => {
wrapper.vm.addSyncOption({ key: 'selectiveSyncShards', value: MOCK_SYNC_SHARDS[0].value });
expect(wrapper.emitted('addSyncOption')).toBeTruthy();
});
});
describe('removeSyncOption', () => {
beforeEach(() => {
createComponent();
wrapper.setProps({
nodeData: { ...propsData.nodeData, selectiveSyncShards: [MOCK_SYNC_SHARDS[0].value] },
});
});
it('should remove value from nodeData', () => {
wrapper.vm.removeSyncOption({ key: 'selectiveSyncShards', index: 0 });
expect(wrapper.emitted('removeSyncOption')).toBeTruthy();
});
});
});
describe('computed', () => {
describe('selectiveSyncShards', () => {
describe('when selectiveSyncType is not `SHARDS`', () => {
beforeEach(() => {
createComponent();
wrapper.setProps({
nodeData: {
...propsData.nodeData,
selectiveSyncType: MOCK_SELECTIVE_SYNC_TYPES.ALL.value,
},
});
});
it('returns `false`', () => {
expect(wrapper.vm.selectiveSyncShards).toBeFalsy();
});
});
describe('when selectiveSyncType is `SHARDS`', () => {
beforeEach(() => {
createComponent();
wrapper.setProps({
nodeData: {
...propsData.nodeData,
selectiveSyncType: MOCK_SELECTIVE_SYNC_TYPES.SHARDS.value,
},
});
});
it('returns `true`', () => {
expect(wrapper.vm.selectiveSyncShards).toBeTruthy();
});
});
});
});
});
import { mount } from '@vue/test-utils';
import { GlIcon, GlDropdown } from '@gitlab/ui';
import GeoNodeFormShards from 'ee/geo_node_form/components/geo_node_form_shards.vue';
import { MOCK_SYNC_SHARDS } from '../mock_data';
describe('GeoNodeFormShards', () => {
let wrapper;
const propsData = {
selectedShards: [],
syncShardsOptions: MOCK_SYNC_SHARDS,
};
const actionSpies = {
toggleShard: jest.fn(),
isSelected: jest.fn(),
};
const createComponent = () => {
wrapper = mount(GeoNodeFormShards, {
propsData,
methods: {
...actionSpies,
},
});
};
afterEach(() => {
wrapper.destroy();
});
const findGlDropdown = () => wrapper.find(GlDropdown);
const findDropdownItems = () => findGlDropdown().findAll('li');
describe('template', () => {
beforeEach(() => {
createComponent();
});
it('renders GlDropdown', () => {
expect(findGlDropdown().exists()).toBe(true);
});
describe('DropdownItems', () => {
beforeEach(() => {
delete actionSpies.isSelected;
createComponent();
wrapper.setProps({
selectedShards: [MOCK_SYNC_SHARDS[0].value],
});
});
it('renders an instance for each shard', () => {
const dropdownItems = findDropdownItems();
dropdownItems.wrappers.forEach((dI, index) => {
expect(dI.html()).toContain(wrapper.vm.syncShardsOptions[index].label);
});
});
it('hides GlIcon if shard not in selectedShards', () => {
const dropdownItems = findDropdownItems();
dropdownItems.wrappers.forEach((dI, index) => {
const dropdownItemIcon = dI.find(GlIcon);
expect(dropdownItemIcon.classes('invisible')).toBe(
!wrapper.vm.isSelected(wrapper.vm.syncShardsOptions[index]),
);
});
});
});
});
describe('methods', () => {
describe('toggleShard', () => {
beforeEach(() => {
delete actionSpies.toggleShard;
});
describe('when shard is in selectedShards', () => {
beforeEach(() => {
createComponent();
wrapper.setProps({
selectedShards: [MOCK_SYNC_SHARDS[0].value],
});
});
it('emits `removeSyncOption`', () => {
wrapper.vm.toggleShard(MOCK_SYNC_SHARDS[0]);
expect(wrapper.emitted('removeSyncOption')).toBeTruthy();
});
});
describe('when shard is not in selectedShards', () => {
beforeEach(() => {
createComponent();
wrapper.setProps({
selectedShards: [MOCK_SYNC_SHARDS[0].value],
});
});
it('emits `addSyncOption`', () => {
wrapper.vm.toggleShard(MOCK_SYNC_SHARDS[1]);
expect(wrapper.emitted('addSyncOption')).toBeTruthy();
});
});
});
describe('isSelected', () => {
beforeEach(() => {
delete actionSpies.isSelected;
});
describe('when shard is in selectedShards', () => {
beforeEach(() => {
createComponent();
wrapper.setProps({
selectedShards: [MOCK_SYNC_SHARDS[0].value],
});
});
it('returns `true`', () => {
expect(wrapper.vm.isSelected(MOCK_SYNC_SHARDS[0])).toBeTruthy();
});
});
describe('when shard is not in selectedShards', () => {
beforeEach(() => {
createComponent();
wrapper.setProps({
selectedShards: [MOCK_SYNC_SHARDS[0].value],
});
});
it('returns `false`', () => {
expect(wrapper.vm.isSelected(MOCK_SYNC_SHARDS[1])).toBeFalsy();
});
});
});
describe('computed', () => {
describe('dropdownTitle', () => {
describe('when selectedShards is empty', () => {
beforeEach(() => {
createComponent();
wrapper.setProps({
selectedShards: [],
});
});
it('returns `Select shards to replicate`', () => {
expect(wrapper.vm.dropdownTitle).toBe('Select shards to replicate');
});
});
describe('when selectedShards is not empty', () => {
beforeEach(() => {
createComponent();
wrapper.setProps({
selectedShards: [MOCK_SYNC_SHARDS[0].value],
});
});
it('returns Shards selected: `this.selectedShards.length`', () => {
expect(wrapper.vm.dropdownTitle).toBe(
`Shards selected: ${wrapper.vm.selectedShards.length}`,
);
});
});
});
describe('noSyncShards', () => {
describe('when syncShardsOptions.length > 0', () => {
beforeEach(() => {
createComponent();
});
it('returns `false`', () => {
expect(wrapper.vm.noSyncShards).toBeFalsy();
});
});
});
describe('when syncShardsOptions.length === 0', () => {
beforeEach(() => {
createComponent();
wrapper.setProps({
syncShardsOptions: [],
});
});
it('returns `true`', () => {
expect(wrapper.vm.noSyncShards).toBeTruthy();
});
});
});
});
});
...@@ -3,7 +3,7 @@ import { visitUrl } from '~/lib/utils/url_utility'; ...@@ -3,7 +3,7 @@ import { visitUrl } from '~/lib/utils/url_utility';
import GeoNodeForm from 'ee/geo_node_form/components/geo_node_form.vue'; import GeoNodeForm from 'ee/geo_node_form/components/geo_node_form.vue';
import GeoNodeFormCore from 'ee/geo_node_form/components/geo_node_form_core.vue'; import GeoNodeFormCore from 'ee/geo_node_form/components/geo_node_form_core.vue';
import GeoNodeFormCapacities from 'ee/geo_node_form/components/geo_node_form_capacities.vue'; import GeoNodeFormCapacities from 'ee/geo_node_form/components/geo_node_form_capacities.vue';
import { MOCK_NODE } from '../mock_data'; import { MOCK_NODE, MOCK_SELECTIVE_SYNC_TYPES, MOCK_SYNC_SHARDS } from '../mock_data';
jest.mock('~/lib/utils/url_utility', () => ({ jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn().mockName('visitUrlMock'), visitUrl: jest.fn().mockName('visitUrlMock'),
...@@ -14,6 +14,8 @@ describe('GeoNodeForm', () => { ...@@ -14,6 +14,8 @@ describe('GeoNodeForm', () => {
const propsData = { const propsData = {
node: MOCK_NODE, node: MOCK_NODE,
selectiveSyncTypes: MOCK_SELECTIVE_SYNC_TYPES,
syncShardsOptions: MOCK_SYNC_SHARDS,
}; };
const createComponent = () => { const createComponent = () => {
...@@ -53,7 +55,9 @@ describe('GeoNodeForm', () => { ...@@ -53,7 +55,9 @@ describe('GeoNodeForm', () => {
showObjectStorage, showObjectStorage,
}) => { }) => {
beforeEach(() => { beforeEach(() => {
wrapper.vm.nodeData.primary = primaryNode; wrapper.setData({
nodeData: { ...wrapper.vm.nodeData, primary: primaryNode },
});
}); });
it(`it ${showCore ? 'shows' : 'hides'} the Core Field`, () => { it(`it ${showCore ? 'shows' : 'hides'} the Core Field`, () => {
...@@ -90,6 +94,33 @@ describe('GeoNodeForm', () => { ...@@ -90,6 +94,33 @@ describe('GeoNodeForm', () => {
expect(visitUrl).toHaveBeenCalled(); expect(visitUrl).toHaveBeenCalled();
}); });
}); });
describe('addSyncOption', () => {
beforeEach(() => {
createComponent();
});
it('should add value to nodeData', () => {
expect(wrapper.vm.nodeData.selectiveSyncShards).toEqual([]);
wrapper.vm.addSyncOption({ key: 'selectiveSyncShards', value: MOCK_SYNC_SHARDS[0].value });
expect(wrapper.vm.nodeData.selectiveSyncShards).toEqual([MOCK_SYNC_SHARDS[0].value]);
});
});
describe('removeSyncOption', () => {
beforeEach(() => {
createComponent();
wrapper.setData({
nodeData: { ...wrapper.vm.nodeData, selectiveSyncShards: [MOCK_SYNC_SHARDS[0].value] },
});
});
it('should remove value from nodeData', () => {
expect(wrapper.vm.nodeData.selectiveSyncShards).toEqual([MOCK_SYNC_SHARDS[0].value]);
wrapper.vm.removeSyncOption({ key: 'selectiveSyncShards', index: 0 });
expect(wrapper.vm.nodeData.selectiveSyncShards).toEqual([]);
});
});
}); });
describe('created', () => { describe('created', () => {
...@@ -99,7 +130,7 @@ describe('GeoNodeForm', () => { ...@@ -99,7 +130,7 @@ describe('GeoNodeForm', () => {
}); });
it('sets nodeData to the correct node', () => { it('sets nodeData to the correct node', () => {
expect(wrapper.vm.nodeData.id).toBe(propsData.node.id); expect(wrapper.vm.nodeData.id).toBe(wrapper.vm.node.id);
}); });
}); });
......
// eslint-disable-next-line import/prefer-default-export export const MOCK_SELECTIVE_SYNC_TYPES = {
ALL: {
label: 'All projects',
value: '',
},
NAMESPACES: {
label: 'Projects in certain groups',
value: 'namespaces',
},
SHARDS: {
label: 'Projects in certain storage shards',
value: 'shards',
},
};
export const MOCK_SYNC_SHARDS = [
{
label: 'Shard 1',
value: 'shard1',
},
{
label: 'Shard 2',
value: 'shard2',
},
{
label: 'Shard 3',
value: 'shard3',
},
];
export const MOCK_NODE = { export const MOCK_NODE = {
id: 1, id: 1,
name: 'Mock Node', name: 'Mock Node',
......
...@@ -3578,6 +3578,9 @@ msgstr "" ...@@ -3578,6 +3578,9 @@ msgstr ""
msgid "Choose which repositories you want to connect and run CI/CD pipelines." msgid "Choose which repositories you want to connect and run CI/CD pipelines."
msgstr "" msgstr ""
msgid "Choose which shards you wish to synchronize to this secondary node"
msgstr ""
msgid "Choose which shards you wish to synchronize to this secondary node." msgid "Choose which shards you wish to synchronize to this secondary node."
msgstr "" msgstr ""
...@@ -12902,6 +12905,9 @@ msgstr "" ...@@ -12902,6 +12905,9 @@ msgstr ""
msgid "Notes|This comment has changed since you started editing, please review the %{open_link}updated comment%{close_link} to ensure information is not lost" msgid "Notes|This comment has changed since you started editing, please review the %{open_link}updated comment%{close_link} to ensure information is not lost"
msgstr "" msgstr ""
msgid "Nothing found…"
msgstr ""
msgid "Nothing to preview." msgid "Nothing to preview."
msgstr "" msgstr ""
...@@ -17025,6 +17031,9 @@ msgstr "" ...@@ -17025,6 +17031,9 @@ msgstr ""
msgid "Select projects you want to import." msgid "Select projects you want to import."
msgstr "" msgstr ""
msgid "Select shards to replicate"
msgstr ""
msgid "Select source branch" msgid "Select source branch"
msgstr "" msgstr ""
...@@ -17055,6 +17064,9 @@ msgstr "" ...@@ -17055,6 +17064,9 @@ 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." 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 "" msgstr ""
msgid "Selective synchronization"
msgstr ""
msgid "Self monitoring project does not exist" msgid "Self monitoring project does not exist"
msgstr "" msgstr ""
...@@ -17385,6 +17397,12 @@ msgstr "" ...@@ -17385,6 +17397,12 @@ msgstr ""
msgid "Severity: %{severity}" msgid "Severity: %{severity}"
msgstr "" msgstr ""
msgid "Shards selected: %{count}"
msgstr ""
msgid "Shards to synchronize"
msgstr ""
msgid "Share" msgid "Share"
msgstr "" msgstr ""
......
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