Commit a2750547 authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch '338231-fix-geo-removal-logic' into 'master'

Geo 2.0 Regression - Add ability to remove primary

See merge request gitlab-org/gitlab!68005
parents 07b79094 a6ec0ef6
...@@ -35,6 +35,7 @@ to do that. ...@@ -35,6 +35,7 @@ to do that.
To remove the **primary** site: To remove the **primary** site:
1. [Remove all secondary Geo sites](remove_geo_site.md)
1. On the top bar, select **Menu >** **{admin}** **Admin**. 1. On the top bar, select **Menu >** **{admin}** **Admin**.
1. On the left sidebar, select **Geo > Nodes**. 1. On the left sidebar, select **Geo > Nodes**.
1. Select **Remove** for the **primary** node. 1. Select **Remove** for the **primary** node.
......
...@@ -11,6 +11,16 @@ Review this page for update instructions for your version. These steps ...@@ -11,6 +11,16 @@ Review this page for update instructions for your version. These steps
accompany the [general steps](updating_the_geo_nodes.md#general-update-steps) accompany the [general steps](updating_the_geo_nodes.md#general-update-steps)
for updating Geo nodes. for updating Geo nodes.
## Updating to GitLab 14.0/14.1
We found an issue where [Primary sites can not be removed from the UI](https://gitlab.com/gitlab-org/gitlab/-/issues/338231).
This bug only exists in the UI and does not block the removal of Primary sites using any other method.
### If you have already updated to an affected version and need to remove your Primary site
You can manually remove the Primary site by using the [Geo Nodes API](../../../api/geo_nodes.md#delete-a-geo-node).
## Updating to GitLab 13.12 ## Updating to GitLab 13.12
We found an issue where [secondary nodes re-download all LFS files](https://gitlab.com/gitlab-org/gitlab/-/issues/334550) upon update. This bug: We found an issue where [secondary nodes re-download all LFS files](https://gitlab.com/gitlab-org/gitlab/-/issues/334550) upon update. This bug:
......
...@@ -14,9 +14,9 @@ export default { ...@@ -14,9 +14,9 @@ export default {
'Geo|With GitLab Geo, you can install a special read-only and replicated instance anywhere. %{linkStart}Learn more%{linkEnd}', 'Geo|With GitLab Geo, you can install a special read-only and replicated instance anywhere. %{linkStart}Learn more%{linkEnd}',
), ),
addSite: s__('Geo|Add site'), addSite: s__('Geo|Add site'),
modalTitle: s__('Geo|Remove secondary node'), modalTitle: s__('Geo|Remove node'),
modalBody: s__( modalBody: s__(
'Geo|Removing a Geo secondary node stops the synchronization to that node. Are you sure?', 'Geo|Removing a Geo node stops the synchronization to and from that node. Are you sure?',
), ),
primarySite: s__('Geo|Primary site'), primarySite: s__('Geo|Primary site'),
secondarySite: s__('Geo|Secondary site'), secondarySite: s__('Geo|Secondary site'),
......
<script> <script>
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import { mapGetters } from 'vuex';
import { __ } from '~/locale'; import { __ } from '~/locale';
export default { export default {
...@@ -17,6 +18,9 @@ export default { ...@@ -17,6 +18,9 @@ export default {
required: true, required: true,
}, },
}, },
computed: {
...mapGetters(['canRemoveNode']),
},
}; };
</script> </script>
...@@ -28,7 +32,7 @@ export default { ...@@ -28,7 +32,7 @@ export default {
<gl-button <gl-button
variant="danger" variant="danger"
category="secondary" category="secondary"
:disabled="node.primary" :disabled="!canRemoveNode(node.id)"
data-testid="geo-desktop-remove-action" data-testid="geo-desktop-remove-action"
@click="$emit('remove')" @click="$emit('remove')"
>{{ $options.i18n.removeButtonLabel }}</gl-button >{{ $options.i18n.removeButtonLabel }}</gl-button
......
<script> <script>
import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
import { mapGetters } from 'vuex';
import { __ } from '~/locale'; import { __ } from '~/locale';
export default { export default {
...@@ -20,8 +21,9 @@ export default { ...@@ -20,8 +21,9 @@ export default {
}, },
}, },
computed: { computed: {
...mapGetters(['canRemoveNode']),
dropdownRemoveClass() { dropdownRemoveClass() {
return this.node.primary ? 'gl-text-gray-400' : 'gl-text-red-500'; return this.canRemoveNode(this.node.id) ? 'gl-text-red-500' : 'gl-text-gray-400';
}, },
}, },
}; };
...@@ -34,7 +36,7 @@ export default { ...@@ -34,7 +36,7 @@ export default {
</template> </template>
<gl-dropdown-item :href="node.webEditUrl">{{ $options.i18n.editButtonLabel }}</gl-dropdown-item> <gl-dropdown-item :href="node.webEditUrl">{{ $options.i18n.editButtonLabel }}</gl-dropdown-item>
<gl-dropdown-item <gl-dropdown-item
:disabled="node.primary" :disabled="!canRemoveNode(node.id)"
data-testid="geo-mobile-remove-action" data-testid="geo-mobile-remove-action"
@click="$emit('remove')" @click="$emit('remove')"
> >
......
...@@ -53,3 +53,9 @@ export const syncInfo = (state) => (id) => { ...@@ -53,3 +53,9 @@ export const syncInfo = (state) => (id) => {
}; };
}); });
}; };
export const canRemoveNode = (state) => (id) => {
const node = state.nodes.find((n) => n.id === id);
return !node.primary || state.nodes.length === 1;
};
import { GlButton } from '@gitlab/ui'; import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import GeoNodeActionsDesktop from 'ee/geo_nodes/components/header/geo_node_actions_desktop.vue'; import GeoNodeActionsDesktop from 'ee/geo_nodes/components/header/geo_node_actions_desktop.vue';
import { MOCK_NODES } from 'ee_jest/geo_nodes/mock_data'; import { MOCK_NODES } from 'ee_jest/geo_nodes/mock_data';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
Vue.use(Vuex);
describe('GeoNodeActionsDesktop', () => { describe('GeoNodeActionsDesktop', () => {
let wrapper; let wrapper;
...@@ -11,9 +15,17 @@ describe('GeoNodeActionsDesktop', () => { ...@@ -11,9 +15,17 @@ describe('GeoNodeActionsDesktop', () => {
node: MOCK_NODES[0], node: MOCK_NODES[0],
}; };
const createComponent = (props) => { const createComponent = (props, getters) => {
const store = new Vuex.Store({
getters: {
canRemoveNode: () => () => true,
...getters,
},
});
wrapper = extendedWrapper( wrapper = extendedWrapper(
shallowMount(GeoNodeActionsDesktop, { shallowMount(GeoNodeActionsDesktop, {
store,
propsData: { propsData: {
...defaultProps, ...defaultProps,
...props, ...props,
...@@ -56,16 +68,16 @@ describe('GeoNodeActionsDesktop', () => { ...@@ -56,16 +68,16 @@ describe('GeoNodeActionsDesktop', () => {
}); });
describe.each` describe.each`
primary | disabled canRemoveNode | disabled
${true} | ${'true'} ${false} | ${'true'}
${false} | ${undefined} ${true} | ${undefined}
`(`conditionally`, ({ primary, disabled }) => { `(`conditionally`, ({ canRemoveNode, disabled }) => {
beforeEach(() => { beforeEach(() => {
createComponent({ node: { primary } }); createComponent({}, { canRemoveNode: () => () => canRemoveNode });
}); });
describe(`when primary is ${primary}`, () => { describe(`when canRemoveNode is ${canRemoveNode}`, () => {
it(`does ${primary ? '' : 'not '}disable the Desktop Remove button`, () => { it(`does ${canRemoveNode ? 'not ' : ''}disable the Desktop Remove button`, () => {
expect(findGeoDesktopActionsRemoveButton().attributes('disabled')).toBe(disabled); expect(findGeoDesktopActionsRemoveButton().attributes('disabled')).toBe(disabled);
}); });
}); });
......
import { GlDropdown, GlDropdownItem } from '@gitlab/ui'; import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import GeoNodeActionsMobile from 'ee/geo_nodes/components/header/geo_node_actions_mobile.vue'; import GeoNodeActionsMobile from 'ee/geo_nodes/components/header/geo_node_actions_mobile.vue';
import { MOCK_NODES } from 'ee_jest/geo_nodes/mock_data'; import { MOCK_NODES } from 'ee_jest/geo_nodes/mock_data';
import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper';
Vue.use(Vuex);
describe('GeoNodeActionsMobile', () => { describe('GeoNodeActionsMobile', () => {
let wrapper; let wrapper;
...@@ -11,9 +15,17 @@ describe('GeoNodeActionsMobile', () => { ...@@ -11,9 +15,17 @@ describe('GeoNodeActionsMobile', () => {
node: MOCK_NODES[0], node: MOCK_NODES[0],
}; };
const createComponent = (props) => { const createComponent = (props, getters) => {
const store = new Vuex.Store({
getters: {
canRemoveNode: () => () => true,
...getters,
},
});
wrapper = extendedWrapper( wrapper = extendedWrapper(
shallowMount(GeoNodeActionsMobile, { shallowMount(GeoNodeActionsMobile, {
store,
propsData: { propsData: {
...defaultProps, ...defaultProps,
...props, ...props,
...@@ -62,17 +74,17 @@ describe('GeoNodeActionsMobile', () => { ...@@ -62,17 +74,17 @@ describe('GeoNodeActionsMobile', () => {
}); });
describe.each` describe.each`
primary | disabled | dropdownClass canRemoveNode | disabled | dropdownClass
${true} | ${'true'} | ${'gl-text-gray-400'} ${false} | ${'true'} | ${'gl-text-gray-400'}
${false} | ${undefined} | ${'gl-text-red-500'} ${true} | ${undefined} | ${'gl-text-red-500'}
`(`conditionally`, ({ primary, disabled, dropdownClass }) => { `(`conditionally`, ({ canRemoveNode, disabled, dropdownClass }) => {
beforeEach(() => { beforeEach(() => {
createComponent({ node: { primary } }); createComponent({}, { canRemoveNode: () => () => canRemoveNode });
}); });
describe(`when primary is ${primary}`, () => { describe(`when canRemoveNode is ${canRemoveNode}`, () => {
it(`does ${ it(`does ${
primary ? '' : 'not ' canRemoveNode ? 'not ' : ''
}disable the Mobile Remove dropdown item and adds proper class`, () => { }disable the Mobile Remove dropdown item and adds proper class`, () => {
expect(findGeoMobileActionsRemoveDropdownItem().attributes('disabled')).toBe(disabled); expect(findGeoMobileActionsRemoveDropdownItem().attributes('disabled')).toBe(disabled);
expect(findGeoMobileActionsRemoveDropdownItem().find('span').classes(dropdownClass)).toBe( expect(findGeoMobileActionsRemoveDropdownItem().find('span').classes(dropdownClass)).toBe(
......
...@@ -51,4 +51,22 @@ describe('GeoNodes Store Getters', () => { ...@@ -51,4 +51,22 @@ describe('GeoNodes Store Getters', () => {
expect(getters.syncInfo(state)(MOCK_NODES[1].id)).toStrictEqual(MOCK_SECONDARY_SYNC_INFO); expect(getters.syncInfo(state)(MOCK_NODES[1].id)).toStrictEqual(MOCK_SECONDARY_SYNC_INFO);
}); });
}); });
describe.each`
nodeToRemove | nodes | canRemove
${MOCK_NODES[0]} | ${[MOCK_NODES[0]]} | ${true}
${MOCK_NODES[0]} | ${MOCK_NODES} | ${false}
${MOCK_NODES[1]} | ${[MOCK_NODES[1]]} | ${true}
${MOCK_NODES[1]} | ${MOCK_NODES} | ${true}
`(`canRemoveNode`, ({ nodeToRemove, nodes, canRemove }) => {
describe(`when node.primary ${nodeToRemove.primary} and total nodes is ${nodes.length}`, () => {
beforeEach(() => {
state.nodes = nodes;
});
it(`should return ${canRemove}`, () => {
expect(getters.canRemoveNode(state)(nodeToRemove.id)).toBe(canRemove);
});
});
});
}); });
...@@ -14966,13 +14966,10 @@ msgstr "" ...@@ -14966,13 +14966,10 @@ msgstr ""
msgid "Geo|Remove node" msgid "Geo|Remove node"
msgstr "" msgstr ""
msgid "Geo|Remove secondary node"
msgstr ""
msgid "Geo|Remove tracking database entry" msgid "Geo|Remove tracking database entry"
msgstr "" msgstr ""
msgid "Geo|Removing a Geo secondary node stops the synchronization to that node. Are you sure?" msgid "Geo|Removing a Geo node stops the synchronization to and from that node. Are you sure?"
msgstr "" msgstr ""
msgid "Geo|Replicated data is verified with the secondary node(s) using checksums" msgid "Geo|Replicated data is verified with the secondary node(s) using checksums"
......
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