Commit 9f3c1b79 authored by Zack Cuddy's avatar Zack Cuddy Committed by Kushal Pandya

Geo Node form in Vue

Vue Components
JS Hook

Ruby Items

HAML
Controllers

Applicaiton settings

app_spec.js

geo_node_form_capacities_spec

geo_node_form_core_spec.js

geo_node_form_namespaces_spec.js

Namespace specs

Selective sync test and paramterized tests

Seletive sync spec

Shards Spec

Finished tests

Lint and locale

Remove selective sync from this MR

Lint and locale
parent 3f628a08
<script>
import GeoNodeForm from './geo_node_form.vue';
export default {
name: 'GeoNodeFormApp',
components: {
GeoNodeForm,
},
props: {
node: {
type: Object,
required: false,
default: null,
},
},
};
</script>
<template>
<article class="geo-node-form-container">
<h3 class="page-title">{{ __('Geo Node Form') }}</h3>
<h3 class="page-title">{{ node ? __('Edit Geo Node') : __('New Geo Node') }}</h3>
<geo-node-form :node="node" />
</article>
</template>
<script>
import { GlFormGroup, GlFormInput, GlFormCheckbox, GlButton } from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility';
import GeoNodeFormCore from './geo_node_form_core.vue';
import GeoNodeFormCapacities from './geo_node_form_capacities.vue';
export default {
name: 'GeoNodeForm',
components: {
GlFormGroup,
GlFormInput,
GlFormCheckbox,
GlButton,
GeoNodeFormCore,
GeoNodeFormCapacities,
},
props: {
node: {
type: Object,
required: false,
default: null,
},
},
data() {
return {
nodeData: {
name: '',
url: '',
primary: false,
internalUrl: '',
selectiveSyncType: '',
selectiveSyncNamespaceIds: [],
selectiveSyncShards: [],
reposMaxCapacity: 25,
filesMaxCapacity: 10,
verificationMaxCapacity: 100,
containerRepositoriesMaxCapacity: 10,
minimumReverificationInterval: 7,
syncObjectStorage: false,
},
};
},
created() {
if (this.node) {
this.nodeData = { ...this.node };
}
},
methods: {
redirect() {
visitUrl('/admin/geo/nodes');
},
},
};
</script>
<template>
<form>
<geo-node-form-core :node-data="nodeData" />
<section class="mt-3 pl-0 col-sm-6">
<gl-form-group>
<gl-form-checkbox id="node-primary-field" v-model="nodeData.primary">{{
__('This is a primary node')
}}</gl-form-checkbox>
</gl-form-group>
<gl-form-group
v-if="nodeData.primary"
:label="__('Internal URL (optional)')"
label-for="node-internal-url-field"
:description="
__(
'The URL defined on the primary node that secondary nodes should use to contact it. Defaults to URL',
)
"
>
<gl-form-input id="node-internal-url-field" v-model="nodeData.internalUrl" type="text" />
</gl-form-group>
<geo-node-form-capacities :node-data="nodeData" />
<gl-form-group
v-if="!nodeData.primary"
:label="__('Object Storage replication')"
label-for="node-object-storage-field"
:description="
__(
'If enabled, and if object storage is enabled, GitLab will handle Object Storage replication using Geo',
)
"
>
<gl-form-checkbox id="node-object-storage-field" v-model="nodeData.syncObjectStorage">{{
__('Allow this secondary node to replicate content on Object Storage')
}}</gl-form-checkbox>
</gl-form-group>
</section>
<section class="d-flex align-items-center mt-4">
<gl-button id="node-save-button" variant="success">{{ __('Save') }}</gl-button>
<gl-button id="node-cancel-button" class="ml-auto" @click="redirect">{{
__('Cancel')
}}</gl-button>
</section>
</form>
</template>
<script>
import { GlFormGroup, GlFormInput } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
name: 'GeoNodeFormCapacities',
components: {
GlFormGroup,
GlFormInput,
},
props: {
nodeData: {
type: Object,
required: true,
},
},
data() {
return {
formGroups: [
{
id: 'node-repository-capacity-field',
label: __('Repository sync capacity'),
description: __(
'Control the maximum concurrency of repository backfill for this secondary node',
),
key: 'reposMaxCapacity',
conditional: 'secondary',
},
{
id: 'node-file-capacity-field',
label: __('File sync capacity'),
description: __(
'Control the maximum concurrency of LFS/attachment backfill for this secondary node',
),
key: 'filesMaxCapacity',
conditional: 'secondary',
},
{
id: 'node-verification-capacity-field',
label: __('Verification capacity'),
description: __(
'Control the maximum concurrency of verification operations for this Geo node',
),
key: 'verificationMaxCapacity',
},
{
id: 'node-container-repository-capacity-field',
label: __('Container repositories sync capacity'),
description: __(
'Control the maximum concurrency of container repository operations for this Geo node',
),
key: 'containerRepositoriesMaxCapacity',
},
{
id: 'node-reverification-interval-field',
label: __('Re-verification interval'),
description: __(
'Control the minimum interval in days that a repository should be reverified for this primary node',
),
key: 'minimumReverificationInterval',
conditional: 'primary',
},
],
};
},
computed: {
visibleFormGroups() {
return this.formGroups.filter(group => {
if (group.conditional) {
return this.nodeData.primary
? group.conditional === 'primary'
: group.conditional === 'secondary';
}
return true;
});
},
},
};
</script>
<template>
<div>
<gl-form-group
v-for="formGroup in visibleFormGroups"
:key="formGroup.id"
:label="formGroup.label"
:label-for="formGroup.id"
:description="formGroup.description"
>
<gl-form-input
:id="formGroup.id"
v-model="nodeData[formGroup.key]"
class="col-sm-3"
type="number"
/>
</gl-form-group>
</div>
</template>
<script>
import { GlFormGroup, GlFormInput, GlSprintf } from '@gitlab/ui';
export default {
name: 'GeoNodeFormCore',
components: {
GlFormGroup,
GlFormInput,
GlSprintf,
},
props: {
nodeData: {
type: Object,
required: true,
},
},
};
</script>
<template>
<section class="form-row">
<gl-form-group class="col-sm-6" :label="__('Name')" label-for="node-name-field">
<template #description>
<gl-sprintf
:message="
__(
'The unique identifier for the Geo node. Must match %{geoNodeName} if it is set in gitlab.rb, otherwise it must match %{externalUrl} with a trailing slash',
)
"
>
<template #geoNodeName>
<code>{{ __('geo_node_name') }}</code>
</template>
<template #externalUrl>
<code>{{ __('external_url') }}</code>
</template>
</gl-sprintf>
</template>
<gl-form-input id="node-name-field" v-model="nodeData.name" type="text" />
</gl-form-group>
<gl-form-group
class="col-sm-6"
:label="__('URL')"
label-for="node-url-field"
:description="__('The user-facing URL of the Geo node')"
>
<gl-form-input id="node-url-field" v-model="nodeData.url" type="text" />
</gl-form-group>
</section>
</template>
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import GeoNodeFormApp from './components/app.vue';
Vue.use(Translate);
......@@ -12,9 +13,22 @@ export default () => {
components: {
GeoNodeFormApp,
},
render(createElement) {
return createElement('geo-node-form-app');
const {
dataset: { nodeData },
} = this.$options.el;
let node;
if (nodeData) {
node = JSON.parse(nodeData);
node = convertObjectPropsToCamelCase(node, { deep: true });
}
return createElement('geo-node-form-app', {
props: {
node,
},
});
},
});
};
......@@ -66,6 +66,7 @@ class Admin::Geo::NodesController < Admin::Geo::ApplicationController
def load_node
@node = GeoNode.find(params[:id])
@serialized_node = GeoNodeSerializer.new.represent(@node).to_json
end
def push_feature_flag
......
# frozen_string_literal: true
class GeoNodeSerializer < BaseSerializer
entity EE::API::Entities::GeoNode
end
- page_title _('Edit Geo Node')
- if Feature.enabled?(:enable_geo_node_form_js)
#js-geo-node-form
#js-geo-node-form{ data: { "node-data" => @serialized_node } }
- else
%h3.page-title
Edit Geo Node
......
import { shallowMount } from '@vue/test-utils';
import GeoNodeFormApp from 'ee/geo_node_form/components/app.vue';
import GeoNodeForm from 'ee/geo_node_form/components/geo_node_form.vue';
import { MOCK_NODE } from '../mock_data';
describe('GeoNodeFormApp', () => {
let wrapper;
const propsData = {
node: undefined,
};
const createComponent = () => {
wrapper = shallowMount(GeoNodeFormApp);
wrapper = shallowMount(GeoNodeFormApp, {
propsData,
});
};
afterEach(() => {
wrapper.destroy();
});
const findGeoNodeFormContainer = () => wrapper.find('.geo-node-form-container');
const findGeoNodeFormTitle = () => wrapper.find('.page-title');
const findGeoForm = () => wrapper.find(GeoNodeForm);
describe('render', () => {
beforeEach(() => {
createComponent();
});
it('the node form container', () => {
expect(findGeoNodeFormContainer().exists()).toBe(true);
it('the Geo Node Form Title', () => {
expect(findGeoNodeFormTitle().exists()).toBe(true);
});
it('the Geo Node Form', () => {
expect(findGeoForm().exists()).toBe(true);
});
});
describe('Geo Node Form Title', () => {
describe('when props.node is undefined', () => {
beforeEach(() => {
createComponent();
});
it('sets title to `New Geo Node`', () => {
expect(findGeoNodeFormTitle().text()).toBe('New Geo Node');
});
});
describe('when props.node is set', () => {
beforeEach(() => {
propsData.node = MOCK_NODE;
createComponent();
});
it('`Geo Node Form` header text', () => {
expect(findGeoNodeFormContainer().text()).toContain('Geo Node Form');
it('sets title to `Edit Geo Node`', () => {
expect(findGeoNodeFormTitle().text()).toBe('Edit Geo Node');
});
});
});
});
import { shallowMount } from '@vue/test-utils';
import GeoNodeFormCapacities from 'ee/geo_node_form/components/geo_node_form_capacities.vue';
import { MOCK_NODE } from '../mock_data';
describe('GeoNodeFormCapacities', () => {
let wrapper;
const propsData = {
nodeData: MOCK_NODE,
};
const createComponent = () => {
wrapper = shallowMount(GeoNodeFormCapacities, {
propsData,
});
};
afterEach(() => {
wrapper.destroy();
});
const findGeoNodeFormRepositoryCapacityField = () =>
wrapper.find('#node-repository-capacity-field');
const findGeoNodeFormFileCapacityField = () => wrapper.find('#node-file-capacity-field');
const findGeoNodeFormVerificationCapacityField = () =>
wrapper.find('#node-verification-capacity-field');
const findGeoNodeFormContainerRepositoryCapacityField = () =>
wrapper.find('#node-container-repository-capacity-field');
const findGeoNodeFormReverificationIntervalField = () =>
wrapper.find('#node-reverification-interval-field');
describe('template', () => {
describe.each`
primaryNode | showRepoCapacity | showFileCapacity | showVerificationCapacity | showContainerCapacity | showReverificationInterval
${true} | ${false} | ${false} | ${true} | ${true} | ${true}
${false} | ${true} | ${true} | ${true} | ${true} | ${false}
`(
`conditional fields`,
({
primaryNode,
showRepoCapacity,
showFileCapacity,
showVerificationCapacity,
showContainerCapacity,
showReverificationInterval,
}) => {
beforeEach(() => {
propsData.nodeData.primary = primaryNode;
createComponent();
});
it(`it ${showRepoCapacity ? 'shows' : 'hides'} the Repository Capacity Field`, () => {
expect(findGeoNodeFormRepositoryCapacityField().exists()).toBe(showRepoCapacity);
});
it(`it ${showFileCapacity ? 'shows' : 'hides'} the File Capacity Field`, () => {
expect(findGeoNodeFormFileCapacityField().exists()).toBe(showFileCapacity);
});
it(`it ${
showVerificationCapacity ? 'shows' : 'hides'
} the Verification Capacity Field`, () => {
expect(findGeoNodeFormVerificationCapacityField().exists()).toBe(
showVerificationCapacity,
);
});
it(`it ${
showContainerCapacity ? 'shows' : 'hides'
} the Container Repository Capacity Field`, () => {
expect(findGeoNodeFormContainerRepositoryCapacityField().exists()).toBe(
showContainerCapacity,
);
});
it(`it ${
showReverificationInterval ? 'shows' : 'hides'
} the Reverification Interval Field`, () => {
expect(findGeoNodeFormReverificationIntervalField().exists()).toBe(
showReverificationInterval,
);
});
},
);
});
describe('computed', () => {
describe('visibleFormGroups', () => {
describe('when nodeData.primary is true', () => {
beforeEach(() => {
propsData.nodeData.primary = true;
createComponent();
});
it('contains conditional form groups for primary', () => {
expect(wrapper.vm.visibleFormGroups.some(g => g.conditional === 'primary')).toBeTruthy();
});
it('does not contain conditional form groups for secondary', () => {
expect(wrapper.vm.visibleFormGroups.some(g => g.conditional === 'secondary')).toBeFalsy();
});
});
describe('when nodeData.primary is false', () => {
beforeEach(() => {
propsData.nodeData.primary = false;
createComponent();
});
it('contains conditional form groups for secondary', () => {
expect(
wrapper.vm.visibleFormGroups.some(g => g.conditional === 'secondary'),
).toBeTruthy();
});
it('does not contain conditional form groups for primary', () => {
expect(wrapper.vm.visibleFormGroups.some(g => g.conditional === 'primary')).toBeFalsy();
});
});
});
});
});
import { shallowMount } from '@vue/test-utils';
import GeoNodeFormCore from 'ee/geo_node_form/components/geo_node_form_core.vue';
import { MOCK_NODE } from '../mock_data';
describe('GeoNodeFormCore', () => {
let wrapper;
const propsData = {
nodeData: MOCK_NODE,
};
const createComponent = () => {
wrapper = shallowMount(GeoNodeFormCore, {
propsData,
});
};
afterEach(() => {
wrapper.destroy();
});
const findGeoNodeFormNameField = () => wrapper.find('#node-name-field');
const findGeoNodeFormUrlField = () => wrapper.find('#node-url-field');
describe('template', () => {
beforeEach(() => {
createComponent();
});
it('renders Geo Node Form Name Field', () => {
expect(findGeoNodeFormNameField().exists()).toBe(true);
});
it('renders Geo Node Form Url Field', () => {
expect(findGeoNodeFormUrlField().exists()).toBe(true);
});
});
});
import { shallowMount } from '@vue/test-utils';
import { visitUrl } from '~/lib/utils/url_utility';
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 GeoNodeFormCapacities from 'ee/geo_node_form/components/geo_node_form_capacities.vue';
import { MOCK_NODE } from '../mock_data';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn().mockName('visitUrlMock'),
}));
describe('GeoNodeForm', () => {
let wrapper;
const propsData = {
node: MOCK_NODE,
};
const createComponent = () => {
wrapper = shallowMount(GeoNodeForm, {
propsData,
});
};
afterEach(() => {
wrapper.destroy();
});
const findGeoNodeFormCoreField = () => wrapper.find(GeoNodeFormCore);
const findGeoNodePrimaryField = () => wrapper.find('#node-primary-field');
const findGeoNodeInternalUrlField = () => wrapper.find('#node-internal-url-field');
const findGeoNodeFormCapacitiesField = () => wrapper.find(GeoNodeFormCapacities);
const findGeoNodeObjectStorageField = () => wrapper.find('#node-object-storage-field');
const findGeoNodeCancelButton = () => wrapper.find('#node-cancel-button');
describe('template', () => {
beforeEach(() => {
createComponent();
});
describe.each`
primaryNode | showCore | showPrimary | showInternalUrl | showCapacities | showObjectStorage
${true} | ${true} | ${true} | ${true} | ${true} | ${false}
${false} | ${true} | ${true} | ${false} | ${true} | ${true}
`(
`conditional fields`,
({
primaryNode,
showCore,
showPrimary,
showInternalUrl,
showCapacities,
showObjectStorage,
}) => {
beforeEach(() => {
wrapper.vm.nodeData.primary = primaryNode;
});
it(`it ${showCore ? 'shows' : 'hides'} the Core Field`, () => {
expect(findGeoNodeFormCoreField().exists()).toBe(showCore);
});
it(`it ${showPrimary ? 'shows' : 'hides'} the Primary Field`, () => {
expect(findGeoNodePrimaryField().exists()).toBe(showPrimary);
});
it(`it ${showInternalUrl ? 'shows' : 'hides'} the Internal URL Field`, () => {
expect(findGeoNodeInternalUrlField().exists()).toBe(showInternalUrl);
});
it(`it ${showCapacities ? 'shows' : 'hides'} the Capacities Field`, () => {
expect(findGeoNodeFormCapacitiesField().exists()).toBe(showCapacities);
});
it(`it ${showObjectStorage ? 'shows' : 'hides'} the Object Storage Field`, () => {
expect(findGeoNodeObjectStorageField().exists()).toBe(showObjectStorage);
});
},
);
});
describe('methods', () => {
describe('redirect', () => {
beforeEach(() => {
createComponent();
});
it('calls visitUrl when cancel is clicked', () => {
findGeoNodeCancelButton().vm.$emit('click');
expect(visitUrl).toHaveBeenCalled();
});
});
});
describe('created', () => {
describe('when node prop exists', () => {
beforeEach(() => {
createComponent();
});
it('sets nodeData to the correct node', () => {
expect(wrapper.vm.nodeData.id).toBe(propsData.node.id);
});
});
describe('when node prop does not exist', () => {
beforeEach(() => {
propsData.node = null;
createComponent();
});
it('sets nodeData to the default node data', () => {
expect(wrapper.vm.nodeData).not.toBeNull();
expect(wrapper.vm.nodeData.id).not.toBe(MOCK_NODE.id);
});
});
});
});
// eslint-disable-next-line import/prefer-default-export
export const MOCK_NODE = {
id: 1,
name: 'Mock Node',
url: 'https://mock_node.gitlab.com',
primary: false,
internalUrl: '',
selectiveSyncType: '',
namespaceIds: [],
selectiveSyncShards: [],
reposMaxCapacity: 25,
filesMaxCapacity: 10,
verificationMaxCapacity: 100,
containerRepositoriesMaxCapacity: 10,
minimumReverificationInterval: 7,
syncObjectStorage: false,
};
......@@ -1598,6 +1598,9 @@ msgstr ""
msgid "Allow this key to push to repository as well? (Default only allows pull access.)"
msgstr ""
msgid "Allow this secondary node to replicate content on Object Storage"
msgstr ""
msgid "Allow users to register any application to use GitLab as an OAuth provider"
msgstr ""
......@@ -5038,6 +5041,9 @@ msgstr ""
msgid "Container registry is not enabled on this GitLab instance. Ask an administrator to enable it in order for Auto DevOps to work."
msgstr ""
msgid "Container repositories sync capacity"
msgstr ""
msgid "ContainerRegistry|Automatically remove extra images that aren't designed to be kept."
msgstr ""
......@@ -5244,9 +5250,21 @@ 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 container repository operations for this Geo 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 "Cookie domain"
msgstr ""
......@@ -8255,6 +8273,9 @@ msgstr ""
msgid "File name"
msgstr ""
msgid "File sync capacity"
msgstr ""
msgid "File templates"
msgstr ""
......@@ -8561,9 +8582,6 @@ msgstr ""
msgid "Geo Designs"
msgstr ""
msgid "Geo Node Form"
msgstr ""
msgid "Geo Nodes"
msgstr ""
......@@ -10042,6 +10060,9 @@ msgstr ""
msgid "If enabled, access to projects will be validated on an external service using their classification label."
msgstr ""
msgid "If enabled, and if object storage is enabled, GitLab will handle Object Storage replication using Geo"
msgstr ""
msgid "If this was a mistake you can %{leave_link_start}leave the %{source_type}%{link_end}."
msgstr ""
......@@ -10353,6 +10374,9 @@ msgstr ""
msgid "Internal - The project can be accessed by any logged in user."
msgstr ""
msgid "Internal URL (optional)"
msgstr ""
msgid "Internal users"
msgstr ""
......@@ -15345,6 +15369,9 @@ msgstr ""
msgid "Re-authentication required"
msgstr ""
msgid "Re-verification interval"
msgstr ""
msgid "Read more"
msgstr ""
......@@ -15853,6 +15880,9 @@ msgstr ""
msgid "Repository storage"
msgstr ""
msgid "Repository sync capacity"
msgstr ""
msgid "Repository: %{counter_repositories} / Wikis: %{counter_wikis} / Build Artifacts: %{counter_build_artifacts} / LFS: %{counter_lfs_objects}"
msgstr ""
......@@ -18570,6 +18600,9 @@ msgstr ""
msgid "The Prometheus server responded with \"bad request\". Please check your queries are correct and are supported in your Prometheus version. %{documentationLink}"
msgstr ""
msgid "The URL defined on the primary node that secondary nodes should use to contact it. Defaults to URL"
msgstr ""
msgid "The URL to use for connecting to Elasticsearch. Use a comma-separated list to support clustering (e.g., \"http://localhost:9200, http://localhost:9201\")."
msgstr ""
......@@ -18851,6 +18884,9 @@ msgstr ""
msgid "The total stage shows the time it takes between creating an issue and deploying the code to production. The data will be automatically added once you have completed the full idea to production cycle."
msgstr ""
msgid "The unique identifier for the Geo node. Must match %{geoNodeName} if it is set in gitlab.rb, otherwise it must match %{externalUrl} with a trailing slash"
msgstr ""
msgid "The unique identifier for the Geo node. Must match `geo_node_name` if it is set in gitlab.rb, otherwise it must match `external_url` with a trailing slash"
msgstr ""
......@@ -19196,6 +19232,9 @@ msgstr ""
msgid "This is a list of devices that have logged into your account. Revoke any sessions that you do not recognize."
msgstr ""
msgid "This is a primary node"
msgstr ""
msgid "This is a security log of important events involving your account."
msgstr ""
......@@ -20871,6 +20910,9 @@ msgstr ""
msgid "Various settings that affect GitLab performance."
msgstr ""
msgid "Verification capacity"
msgstr ""
msgid "Verification information"
msgstr ""
......@@ -22423,6 +22465,9 @@ msgstr ""
msgid "expires on %{milestone_due_date}"
msgstr ""
msgid "external_url"
msgstr ""
msgid "failed"
msgstr ""
......@@ -22453,6 +22498,9 @@ msgstr ""
msgid "from"
msgstr ""
msgid "geo_node_name"
msgstr ""
msgid "group"
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