Commit 806855d6 authored by Serena Fang's avatar Serena Fang Committed by Natalia Tepluhina

Move environment scope to Vue

Move environment scope form input to Vue, remove from haml file. Add
specs for testing environment scope input
parent 02d90306
......@@ -8,14 +8,7 @@ import Flash from '../flash';
import Poll from '../lib/utils/poll';
import initSettingsPanels from '../settings_panels';
import eventHub from './event_hub';
import {
APPLICATION_STATUS,
INGRESS,
INGRESS_DOMAIN_SUFFIX,
CROSSPLANE,
KNATIVE,
FLUENTD,
} from './constants';
import { APPLICATION_STATUS, CROSSPLANE, KNATIVE, FLUENTD } from './constants';
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
import Applications from './components/applications.vue';
......@@ -120,10 +113,6 @@ export default class Clusters {
this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason');
this.successApplicationContainer = document.querySelector('.js-cluster-application-notice');
this.tokenField = document.querySelector('.js-cluster-token');
this.ingressDomainHelpText = document.querySelector('.js-ingress-domain-help-text');
this.ingressDomainSnippet =
this.ingressDomainHelpText &&
this.ingressDomainHelpText.querySelector('.js-ingress-domain-snippet');
initProjectSelectDropdown();
Clusters.initDismissableCallout();
......@@ -327,13 +316,6 @@ export default class Clusters {
this.checkForNewInstalls(prevApplicationMap, this.store.state.applications);
this.updateContainer(prevStatus, this.store.state.status, this.store.state.statusReason);
if (this.ingressDomainHelpText) {
this.toggleIngressDomainHelpText(
prevApplicationMap[INGRESS],
this.store.state.applications[INGRESS],
);
}
if (this.store.state.applications[KNATIVE]?.status === APPLICATION_STATUS.INSTALLED) {
initServerlessSurveyBanner();
}
......@@ -505,13 +487,6 @@ export default class Clusters {
});
}
toggleIngressDomainHelpText({ externalIp }, { externalIp: newExternalIp }) {
if (externalIp !== newExternalIp) {
this.ingressDomainHelpText.classList.toggle('hide', !newExternalIp);
this.ingressDomainSnippet.textContent = `${newExternalIp}${INGRESS_DOMAIN_SUFFIX}`;
}
}
saveKnativeDomain(data) {
const appId = data.id;
this.store.updateApplication(appId);
......
<script>
import { GlFormGroup, GlToggle, GlTooltipDirective } from '@gitlab/ui';
import {
GlFormGroup,
GlFormInput,
GlToggle,
GlTooltipDirective,
GlSprintf,
GlLink,
GlButton,
} from '@gitlab/ui';
import { mapState } from 'vuex';
export default {
components: {
GlFormGroup,
GlToggle,
GlFormInput,
GlSprintf,
GlLink,
GlButton,
},
directives: {
GlTooltip: GlTooltipDirective,
},
inject: {
autoDevopsHelpPath: {
type: String,
},
externalEndpointHelpPath: {
type: String,
},
},
data() {
return {
toggleEnabled: true,
envScope: '*',
baseDomainField: '',
externalIp: '',
};
},
computed: {
...mapState(['enabled', 'editable']),
...mapState([
'enabled',
'editable',
'environmentScope',
'baseDomain',
'applicationIngressExternalIp',
]),
canSubmit() {
return (
this.enabled !== this.toggleEnabled ||
this.environmentScope !== this.envScope ||
this.baseDomain !== this.baseDomainField
);
},
},
mounted() {
this.toggleEnabled = this.enabled;
this.envScope = this.environmentScope;
this.baseDomainField = this.baseDomain;
this.externalIp = this.applicationIngressExternalIp;
},
};
</script>
<template>
<div class="d-flex align-items-center">
<div class="d-flex gl-flex-direction-column">
<gl-form-group>
<div class="gl-display-flex gl-align-items-center">
<h4 class="gl-pr-3 gl-m-0 ">{{ s__('ClusterIntegration|GitLab Integration') }}</h4>
<input
id="cluster_enabled"
class="js-project-feature-toggle-input"
type="hidden"
:value="toggleEnabled"
name="cluster[enabled]"
/>
<div id="tooltipcontainer" class="js-cluster-enable-toggle-area">
<h4 class="gl-pr-3 gl-m-0">{{ s__('ClusterIntegration|GitLab Integration') }}</h4>
<div class="js-cluster-enable-toggle-area">
<gl-toggle
id="toggleCluster"
v-model="toggleEnabled"
v-gl-tooltip:tooltipcontainer
name="cluster[enabled]"
class="gl-mb-0 js-project-feature-toggle"
data-qa-selector="integration_status_toggle"
:aria-describedby="__('Toggle Kubernetes cluster')"
aria-describedby="toggleCluster"
:disabled="!editable"
:is_checked="toggleEnabled"
:title="
s__(
'ClusterIntegration|Enable or disable GitLab\'s connection to your Kubernetes cluster.',
......@@ -54,5 +88,76 @@ export default {
</div>
</div>
</gl-form-group>
<gl-form-group
:label="s__('ClusterIntegration|Environment scope')"
label-size="sm"
label-for="cluster_environment_scope"
:description="
s__('ClusterIntegration|Choose which of your environments will use this cluster.')
"
>
<gl-form-input
id="cluster_environment_scope"
v-model="envScope"
name="cluster[environment_scope]"
class="col-md-6"
type="text"
/>
</gl-form-group>
<gl-form-group
:label="s__('ClusterIntegration|Base domain')"
label-size="sm"
label-for="cluster_base_domain"
>
<gl-form-input
id="cluster_base_domain"
v-model="baseDomainField"
name="cluster[base_domain]"
data-qa-selector="base_domain_field"
class="col-md-6"
type="text"
/>
<div class="form-text text-muted inline">
<gl-sprintf
:message="
s__(
'ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{linkStart}Auto DevOps.%{linkEnd} The domain should have a wildcard DNS configured matching the domain. ',
)
"
>
<template #link="{ content }">
<gl-link :href="autoDevopsHelpPath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
<div v-if="applicationIngressExternalIp" class="js-ingress-domain-help-text inline">
{{ s__('ClusterIntegration|Alternatively, ') }}
<gl-sprintf :message="s__('ClusterIntegration|%{externalIp}.nip.io')">
<template #externalIp>{{ externalIp }}</template>
</gl-sprintf>
{{ s__('ClusterIntegration|can be used instead of a custom domain. ') }}
</div>
<gl-sprintf
class="inline"
:message="s__('ClusterIntegration|%{linkStart}More information%{linkEnd}')"
>
<template #link="{ content }">
<gl-link :href="externalEndpointHelpPath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</div>
</gl-form-group>
<div v-if="editable" class="form group gl-display-flex gl-justify-content-end">
<gl-button
category="primary"
variant="success"
type="submit"
:disabled="!canSubmit"
:aria-disabled="!canSubmit"
data-qa-selector="save_changes_button"
>{{ s__('ClusterIntegration|Save changes') }}</gl-button
>
</div>
</div>
</template>
......@@ -9,13 +9,19 @@ export default () => {
return;
}
const { autoDevopsHelpPath, externalEndpointHelpPath } = entryPoint.dataset;
// eslint-disable-next-line no-new
new Vue({
el: entryPoint,
store: createStore(entryPoint.dataset),
provide: {
autoDevopsHelpPath,
externalEndpointHelpPath,
},
render(createElement) {
return createElement(IntegrationForm);
return createElement(IntegrationForm, {});
},
});
};
......@@ -4,5 +4,10 @@ export default (initialState = {}) => {
return {
enabled: parseBoolean(initialState.enabled),
editable: parseBoolean(initialState.editable),
environmentScope: initialState.environmentScope,
baseDomain: initialState.baseDomain,
applicationIngressExternalIp: initialState.applicationIngressExternalIp,
autoDevopsHelpPath: initialState.autoDevopsHelpPath,
externalEndpointHelpPath: initialState.externalEndpointHelpPath,
};
};
# frozen_string_literal: true
module ClustersHelper
def has_multiple_clusters?
true
end
def create_new_cluster_label(provider: nil)
case provider
when 'aws'
......@@ -31,7 +27,12 @@ module ClustersHelper
def js_cluster_form_data(cluster, can_edit)
{
enabled: cluster.enabled?.to_s,
editable: can_edit.to_s
editable: can_edit.to_s,
environment_scope: cluster.environment_scope,
base_domain: cluster.base_domain,
application_ingress_external_ip: cluster.application_ingress_external_ip,
auto_devops_help_path: help_page_path('topics/autodevops/index'),
external_endpoint_help_path: help_page_path('user/clusters/applications.md', anchor: 'pointing-your-dns-at-the-external-endpoint')
}
end
......
= form_for @cluster, url: clusterable.cluster_path(@cluster), as: :cluster, html: { class: 'js-cluster-integration-form' } do |field|
= form_errors(@cluster)
#js-cluster-integration-form{ data: js_cluster_form_data(@cluster, can?(current_user, :update_cluster, @cluster)) }
.form-group
%h5= s_('ClusterIntegration|Environment scope')
= field.text_field :environment_scope, class: 'col-md-6 form-control js-select-on-focus', placeholder: s_('ClusterIntegration|Environment scope')
- environment_scope_url = help_page_path('user/project/clusters/index', anchor: 'base-domain')
- environment_scope_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: environment_scope_url }
.form-text.text-muted= s_("ClusterIntegration|Choose which of your environments will use this cluster. %{environment_scope_start}More information%{environment_scope_end}").html_safe % { environment_scope_start: environment_scope_start, environment_scope_end: '</a>'.html_safe }
.form-group
%h5= s_('ClusterIntegration|Base domain')
= field.text_field :base_domain, class: 'col-md-6 form-control js-select-on-focus', data: { qa_selector: 'base_domain_field' }
.form-text.text-muted
- auto_devops_url = help_page_path('topics/autodevops/index')
- auto_devops_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: auto_devops_url }
= s_('ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{auto_devops_start}Auto DevOps%{auto_devops_end}. The domain should have a wildcard DNS configured matching the domain.').html_safe % { auto_devops_start: auto_devops_start, auto_devops_end: '</a>'.html_safe }
%span{ :class => ["js-ingress-domain-help-text", ("hide" unless @cluster.application_ingress_external_ip.present?)] }
= s_('ClusterIntegration|Alternatively')
%code{ :class => "js-ingress-domain-snippet" }
= s_('ClusterIntegration|%{external_ip}.nip.io').html_safe % { external_ip: @cluster.application_ingress_external_ip }
= s_('ClusterIntegration| can be used instead of a custom domain.')
- custom_domain_url = help_page_path('user/clusters/applications.md', anchor: 'pointing-your-dns-at-the-external-endpoint')
- custom_domain_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: custom_domain_url }
= s_('ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}.').html_safe % { custom_domain_start: custom_domain_start, custom_domain_end: '</a>'.html_safe }
- if can?(current_user, :update_cluster, @cluster)
.form-group.gl-display-flex.gl-justify-content-end
= field.submit _('Save changes'), class: 'btn btn-success', data: { qa_selector: 'save_changes_button'}
......@@ -5096,19 +5096,16 @@ msgstr ""
msgid "Cluster type must be specificed for Stages::ClusterEndpointInserter"
msgstr ""
msgid "ClusterIntegration| %{custom_domain_start}More information%{custom_domain_end}."
msgstr ""
msgid "ClusterIntegration| This will permanently delete the following resources: <ul> <li>All installed applications and related resources</li> <li>The <code>gitlab-managed-apps</code> namespace</li> <li>Any project namespaces</li> <li><code>clusterroles</code></li> <li><code>clusterrolebindings</code></li> </ul>"
msgstr ""
msgid "ClusterIntegration| can be used instead of a custom domain."
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
msgstr ""
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
msgid "ClusterIntegration|%{externalIp}.nip.io"
msgstr ""
msgid "ClusterIntegration|%{external_ip}.nip.io"
msgid "ClusterIntegration|%{linkStart}More information%{linkEnd}"
msgstr ""
msgid "ClusterIntegration|%{title} uninstalled successfully."
......@@ -5162,7 +5159,7 @@ msgstr ""
msgid "ClusterIntegration|Allow GitLab to manage namespace and service accounts for this cluster. %{startLink}More information%{endLink}"
msgstr ""
msgid "ClusterIntegration|Alternatively"
msgid "ClusterIntegration|Alternatively, "
msgstr ""
msgid "ClusterIntegration|Amazon EKS"
......@@ -5225,9 +5222,6 @@ msgstr ""
msgid "ClusterIntegration|Choose which of your environments will use this cluster."
msgstr ""
msgid "ClusterIntegration|Choose which of your environments will use this cluster. %{environment_scope_start}More information%{environment_scope_end}"
msgstr ""
msgid "ClusterIntegration|Clear cluster cache"
msgstr ""
......@@ -5849,7 +5843,7 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong while updating Knative domain name."
msgstr ""
msgid "ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{auto_devops_start}Auto DevOps%{auto_devops_end}. The domain should have a wildcard DNS configured matching the domain."
msgid "ClusterIntegration|Specifying a domain will allow you to use Auto Review Apps and Auto Deploy stages for %{linkStart}Auto DevOps.%{linkEnd} The domain should have a wildcard DNS configured matching the domain. "
msgstr ""
msgid "ClusterIntegration|Subnets"
......@@ -5972,6 +5966,9 @@ msgstr ""
msgid "ClusterIntegration|access to Google Kubernetes Engine"
msgstr ""
msgid "ClusterIntegration|can be used instead of a custom domain. "
msgstr ""
msgid "ClusterIntegration|documentation"
msgstr ""
......@@ -25665,9 +25662,6 @@ msgstr ""
msgid "Today"
msgstr ""
msgid "Toggle Kubernetes cluster"
msgstr ""
msgid "Toggle Markdown preview"
msgstr ""
......
......@@ -12,9 +12,6 @@ module QA
view 'app/assets/javascripts/clusters/forms/components/integration_form.vue' do
element :integration_status_toggle, required: true
end
view 'app/views/clusters/clusters/_gitlab_integration_form.html.haml' do
element :base_domain_field, required: true
element :save_changes_button, required: true
end
......
......@@ -20,7 +20,7 @@ RSpec.describe 'Clusterable > Show page' do
expect(page).to have_content(cluster_type_label)
end
it 'allow the user to set domain' do
it 'allow the user to set domain', :js do
visit cluster_path
within '.js-cluster-integration-form' do
......@@ -28,20 +28,19 @@ RSpec.describe 'Clusterable > Show page' do
click_on 'Save changes'
end
expect(page.status_code).to eq(200)
expect(page).to have_content('Kubernetes cluster was successfully updated.')
end
context 'when there is a cluster with ingress and external ip' do
context 'when there is a cluster with ingress and external ip', :js do
before do
cluster.create_application_ingress!(external_ip: '192.168.1.100')
visit cluster_path
end
it 'shows help text with the domain as an alternative to custom domain' do
it 'shows help text with the domain as an alternative to custom domain', :js do
within '.js-cluster-integration-form' do
expect(find(cluster_ingress_help_text_selector)).not_to match_css(hide_modifier_selector)
expect(find(cluster_ingress_help_text_selector).text).to include('192.168.1.100')
end
end
end
......@@ -51,7 +50,7 @@ RSpec.describe 'Clusterable > Show page' do
visit cluster_path
within '.js-cluster-integration-form' do
expect(find(cluster_ingress_help_text_selector)).to match_css(hide_modifier_selector)
expect(page).not_to have_selector(cluster_ingress_help_text_selector)
end
end
end
......
......@@ -2,12 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import { loadHTMLFixture } from 'helpers/fixtures';
import { setTestTimeout } from 'helpers/timeout';
import Clusters from '~/clusters/clusters_bundle';
import {
APPLICATION_STATUS,
INGRESS_DOMAIN_SUFFIX,
APPLICATIONS,
RUNNER,
} from '~/clusters/constants';
import { APPLICATION_STATUS, APPLICATIONS, RUNNER } from '~/clusters/constants';
import axios from '~/lib/utils/axios_utils';
import initProjectSelectDropdown from '~/project_select';
......@@ -308,7 +303,6 @@ describe('Clusters', () => {
return promise.then(() => {
expect(cluster.store.state.applications.helm.status).toEqual(INSTALLED);
expect(cluster.store.state.applications.helm.uninstallFailed).toBe(true);
expect(cluster.store.state.applications.helm.requestReason).toBeDefined();
});
});
......@@ -334,10 +328,8 @@ describe('Clusters', () => {
describe('handleClusterStatusSuccess', () => {
beforeEach(() => {
jest.spyOn(cluster.store, 'updateStateFromServer').mockReturnThis();
jest.spyOn(cluster, 'toggleIngressDomainHelpText').mockReturnThis();
jest.spyOn(cluster, 'checkForNewInstalls').mockReturnThis();
jest.spyOn(cluster, 'updateContainer').mockReturnThis();
cluster.handleClusterStatusSuccess({ data: {} });
});
......@@ -349,53 +341,11 @@ describe('Clusters', () => {
expect(cluster.checkForNewInstalls).toHaveBeenCalled();
});
it('toggles ingress domain help text', () => {
expect(cluster.toggleIngressDomainHelpText).toHaveBeenCalled();
});
it('updates message containers', () => {
expect(cluster.updateContainer).toHaveBeenCalled();
});
});
describe('toggleIngressDomainHelpText', () => {
let ingressPreviousState;
let ingressNewState;
beforeEach(() => {
ingressPreviousState = { externalIp: null };
ingressNewState = { externalIp: '127.0.0.1' };
});
describe(`when ingress have an external ip assigned`, () => {
beforeEach(() => {
cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState);
});
it('displays custom domain help text', () => {
expect(cluster.ingressDomainHelpText.classList.contains('hide')).toEqual(false);
});
it('updates ingress external ip address', () => {
expect(cluster.ingressDomainSnippet.textContent).toEqual(
`${ingressNewState.externalIp}${INGRESS_DOMAIN_SUFFIX}`,
);
});
});
describe(`when ingress does not have an external ip assigned`, () => {
it('hides custom domain help text', () => {
ingressPreviousState.externalIp = '127.0.0.1';
ingressNewState.externalIp = null;
cluster.ingressDomainHelpText.classList.remove('hide');
cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState);
expect(cluster.ingressDomainHelpText.classList.contains('hide')).toEqual(true);
});
});
});
describe('updateApplication', () => {
const params = { version: '1.0.0' };
let storeUpdateApplication;
......
import Vuex from 'vuex';
import IntegrationForm from '~/clusters/forms/components/integration_form.vue';
import { createStore } from '~/clusters/forms/stores/index';
import { mount } from '@vue/test-utils';
import { GlToggle } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlToggle, GlButton } from '@gitlab/ui';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('ClusterIntegrationForm', () => {
let wrapper;
let store;
const glToggle = () => wrapper.find(GlToggle);
const toggleButton = () => glToggle().find('button');
const toggleInput = () => wrapper.find('input');
const defaultStoreValues = {
enabled: true,
editable: true,
environmentScope: '*',
baseDomain: 'testDomain',
applicationIngressExternalIp: null,
};
const createWrapper = () => {
store = createStore({
enabled: 'true',
editable: 'true',
const createWrapper = (storeValues = defaultStoreValues) => {
wrapper = shallowMount(IntegrationForm, {
localVue,
store: createStore(storeValues),
provide: {
autoDevopsHelpPath: 'topics/autodevops/index',
externalEndpointHelpPath: 'user/clusters/applications.md',
},
});
wrapper = mount(IntegrationForm, { store });
return wrapper.vm.$nextTick();
};
beforeEach(() => {
return createWrapper();
});
const destroyWrapper = () => {
wrapper.destroy();
wrapper = null;
};
const findSubmitButton = () => wrapper.find(GlButton);
const findGlToggle = () => wrapper.find(GlToggle);
afterEach(() => {
wrapper.destroy();
destroyWrapper();
});
it('creates the toggle and label', () => {
expect(wrapper.text()).toContain('GitLab Integration');
expect(wrapper.contains(GlToggle)).toBe(true);
});
describe('rendering', () => {
beforeEach(() => createWrapper());
it('enables toggle if editable is true', () => {
expect(findGlToggle().props('disabled')).toBe(false);
});
it('sets the envScope to default', () => {
expect(wrapper.find('[id="cluster_environment_scope"]').attributes('value')).toBe('*');
});
it('sets the baseDomain to default', () => {
expect(wrapper.find('[id="cluster_base_domain"]').attributes('value')).toBe('testDomain');
});
it('initializes toggle with store value', () => {
expect(toggleButton().classes()).toContain('is-checked');
expect(toggleInput().attributes('value')).toBe('true');
describe('when editable is false', () => {
beforeEach(() => {
createWrapper({ ...defaultStoreValues, editable: false });
});
it('disables toggle if editable is false', () => {
expect(findGlToggle().props('disabled')).toBe(true);
});
it('does not render the save button', () => {
expect(findSubmitButton().exists()).toBe(false);
});
});
it('does not render external IP block if applicationIngressExternalIp was not passed', () => {
createWrapper({ ...defaultStoreValues });
expect(wrapper.find('.js-ingress-domain-help-text').exists()).toBe(false);
});
it('renders external IP block if applicationIngressExternalIp was passed', () => {
createWrapper({ ...defaultStoreValues, applicationIngressExternalIp: '127.0.0.1' });
expect(wrapper.find('.js-ingress-domain-help-text').exists()).toBe(true);
});
});
it('switches the toggle value on click', () => {
toggleButton().trigger('click');
wrapper.vm.$nextTick(() => {
expect(toggleButton().classes()).not.toContain('is-checked');
expect(toggleInput().attributes('value')).toBe('false');
describe('reactivity', () => {
beforeEach(() => createWrapper());
it('enables the submit button on changing toggle to different value', () => {
return wrapper.vm
.$nextTick()
.then(() => {
// setData is a bad approach because it changes the internal implementation which we should not touch
// but our GlFormInput lacks the ability to set a new value.
wrapper.setData({ toggleEnabled: !defaultStoreValues.enabled });
})
.then(() => {
expect(findSubmitButton().props('disabled')).toBe(false);
});
});
it('enables the submit button on changing input values', () => {
return wrapper.vm
.$nextTick()
.then(() => {
wrapper.setData({ envScope: `${defaultStoreValues.environmentScope}1` });
})
.then(() => {
expect(findSubmitButton().props('disabled')).toBe(false);
});
});
});
});
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