Commit 3475c813 authored by Zack Cuddy's avatar Zack Cuddy Committed by Denys Mishunov

Maintenance Mode - Hook up API

This change hooks up the Application
Settings API to the maintance view
UI.

This is done via a Vuex store similar to
Geo Settings.
parent cbe15fff
<script>
import { GlToggle, GlFormGroup, GlFormTextarea, GlButton } from '@gitlab/ui';
export default {
name: 'MaintenanceModeSettingsApp',
components: {
GlToggle,
GlFormGroup,
GlFormTextarea,
GlButton,
},
data() {
return {
inMaintenanceMode: false,
bannerMessage: '',
};
},
};
</script>
<template>
<article>
<div class="d-flex align-items-center mb-3">
<gl-toggle v-model="inMaintenanceMode" class="mb-0" />
<div class="ml-2">
<p class="mb-0">{{ __('Enable maintenance mode') }}</p>
<p class="mb-0 text-secondary-500">
{{
__('Non-admin users can sign in with read-only access and make read-only API requests.')
}}
</p>
</div>
</div>
<gl-form-group label="Banner Message" label-for="maintenanceBannerMessage">
<gl-form-textarea
id="maintenanceBannerMessage"
v-model="bannerMessage"
:placeholder="
__('This GitLab instance is undergoing maintenance and is operating in read-only mode.')
"
/>
</gl-form-group>
<div class="mt-4">
<gl-button variant="success" category="primary">{{ __('Save changes') }}</gl-button>
</div>
</article>
</template>
import initSettingsPanels from '~/settings_panels';
import projectSelect from '~/project_select';
import selfMonitor from '~/self_monitor';
import maintenanceModeSettings from '~/maintenance_mode_settings';
import initVariableList from '~/ci_variable_list';
document.addEventListener('DOMContentLoaded', () => {
......@@ -9,7 +8,6 @@ document.addEventListener('DOMContentLoaded', () => {
initVariableList('js-instance-variables');
}
selfMonitor();
maintenanceModeSettings();
// Initialize expandable settings panels
initSettingsPanels();
projectSelect();
......
......@@ -103,18 +103,7 @@
= s_('IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox Live Preview.')
= f.submit _('Save changes'), class: "gl-button btn btn-success"
- if Feature.enabled?(:maintenance_mode)
%section.settings.no-animate#js-maintenance-mode-toggle{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
= _('Maintenance mode')
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Prevent users from performing write operations on GitLab while performing maintenance.')
.settings-content
#js-maintenance-mode-settings
= render_if_exists 'admin/application_settings/maintenance_mode_settings_form'
= render_if_exists 'admin/application_settings/elasticsearch_form'
= render 'admin/application_settings/gitpod'
= render 'admin/application_settings/kroki'
......
<script>
import { mapActions, mapState } from 'vuex';
import { GlForm, GlToggle, GlFormGroup, GlFormTextarea, GlButton, GlLoadingIcon } from '@gitlab/ui';
import { __ } from '~/locale';
import { mapComputed } from '~/vuex_shared/bindings';
export default {
name: 'MaintenanceModeSettingsApp',
i18n: {
toggleLabel: __('Enable maintenance mode'),
toggleHelpText: __(
'Non-admin users can sign in with read-only access and make read-only API requests.',
),
bannerMessagePlaceholder: __(
'GitLab is undergoing maintenance and is operating in a read-only mode.',
),
buttonText: __('Save changes'),
},
components: {
GlForm,
GlToggle,
GlFormGroup,
GlFormTextarea,
GlButton,
GlLoadingIcon,
},
computed: {
...mapState(['loading']),
...mapComputed([
{ key: 'maintenanceEnabled', updateFn: 'setMaintenanceEnabled' },
{ key: 'bannerMessage', updateFn: 'setBannerMessage' },
]),
},
methods: {
...mapActions(['updateMaintenanceModeSettings']),
},
};
</script>
<template>
<section>
<gl-loading-icon v-if="loading" size="xl" />
<gl-form v-else @submit.prevent="updateMaintenanceModeSettings">
<div class="gl-display-flex gl-align-items-center gl-mb-4">
<gl-toggle v-model="maintenanceEnabled" />
<div class="gl-ml-3">
<p class="gl-mb-0">{{ $options.i18n.toggleLabel }}</p>
<p class="gl-mb-0 gl-text-gray-500">
{{ $options.i18n.toggleHelpText }}
</p>
</div>
</div>
<gl-form-group label="Banner Message" label-for="maintenanceBannerMessage">
<gl-form-textarea
id="maintenanceBannerMessage"
v-model="bannerMessage"
:placeholder="$options.i18n.bannerMessagePlaceholder"
/>
</gl-form-group>
<gl-button variant="success" type="submit">{{ $options.i18n.buttonText }}</gl-button>
</gl-form>
</section>
</template>
export const DEFAULT_MAINTENANCE_ENABLED = false;
export const DEFAULT_BANNER_MESSAGE = '';
import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import { createStore } from './store';
import MaintenanceModeSettingsApp from './components/app.vue';
Vue.use(Translate);
export default () => {
export const initMaintenanceModeSettings = () => {
const el = document.getElementById('js-maintenance-mode-settings');
if (!el) {
return false;
}
const { maintenanceEnabled, bannerMessage } = el.dataset;
return new Vue({
el,
components: {
MaintenanceModeSettingsApp,
},
store: createStore({ maintenanceEnabled, bannerMessage }),
render(createElement) {
return createElement('maintenance-mode-settings-app');
return createElement(MaintenanceModeSettingsApp);
},
});
};
import Api from 'ee/api';
import createFlash from '~/flash';
import { __ } from '~/locale';
import * as types from './mutation_types';
export const updateMaintenanceModeSettings = ({ commit, state }) => {
commit(types.REQUEST_UPDATE_MAINTENANCE_MODE_SETTINGS);
Api.updateApplicationSettings({
maintenance_mode: state.maintenanceEnabled,
maintenance_mode_message: state.bannerMessage,
})
.then(({ data }) => {
commit(types.RECEIVE_UPDATE_MAINTENANCE_MODE_SETTINGS_SUCCESS, {
maintenanceEnabled: data.maintenance_mode,
bannerMessage: data.maintenance_mode_message,
});
})
.catch(() => {
createFlash({ message: __('There was an error updating the Maintenance Mode Settings') });
commit(types.RECEIVE_UPDATE_MAINTENANCE_MODE_SETTINGS_ERROR);
});
};
export const setMaintenanceEnabled = ({ commit }, { maintenanceEnabled }) => {
commit(types.SET_MAINTENANCE_ENABLED, maintenanceEnabled);
};
export const setBannerMessage = ({ commit }, { bannerMessage }) => {
commit(types.SET_BANNER_MESSAGE, bannerMessage);
};
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import mutations from './mutations';
import { createState } from './state';
Vue.use(Vuex);
export const getStoreConfig = ({ maintenanceEnabled, bannerMessage }) => ({
actions,
mutations,
state: createState({ maintenanceEnabled, bannerMessage }),
});
export const createStore = config => new Vuex.Store(getStoreConfig(config));
export const REQUEST_UPDATE_MAINTENANCE_MODE_SETTINGS = 'REQUEST_UPDATE_MAINTENANCE_MODE_SETTINGS';
export const RECEIVE_UPDATE_MAINTENANCE_MODE_SETTINGS_SUCCESS =
'RECEIVE_UPDATE_MAINTENANCE_MODE_SETTINGS_SUCCESS';
export const RECEIVE_UPDATE_MAINTENANCE_MODE_SETTINGS_ERROR =
'RECEIVE_UPDATE_MAINTENANCE_MODE_SETTINGS_ERROR';
export const SET_MAINTENANCE_ENABLED = 'SET_MAINTENANCE_ENABLED';
export const SET_BANNER_MESSAGE = 'SET_BANNER_MESSAGE';
import * as types from './mutation_types';
import { DEFAULT_MAINTENANCE_ENABLED, DEFAULT_BANNER_MESSAGE } from '../constants';
export default {
[types.REQUEST_UPDATE_MAINTENANCE_MODE_SETTINGS](state) {
state.loading = true;
},
[types.RECEIVE_UPDATE_MAINTENANCE_MODE_SETTINGS_SUCCESS](
state,
{ maintenanceEnabled, bannerMessage },
) {
state.loading = false;
state.maintenanceEnabled = maintenanceEnabled;
state.bannerMessage = bannerMessage;
},
[types.RECEIVE_UPDATE_MAINTENANCE_MODE_SETTINGS_ERROR](state) {
state.loading = false;
state.maintenanceEnabled = DEFAULT_MAINTENANCE_ENABLED;
state.bannerMessage = DEFAULT_BANNER_MESSAGE;
},
[types.SET_MAINTENANCE_ENABLED](state, maintenanceEnabled) {
state.maintenanceEnabled = maintenanceEnabled;
},
[types.SET_BANNER_MESSAGE](state, bannerMessage) {
state.bannerMessage = bannerMessage;
},
};
export const createState = ({ maintenanceEnabled, bannerMessage }) => ({
loading: false,
maintenanceEnabled,
bannerMessage,
});
import '~/pages/admin/application_settings/general/index';
import { initMaintenanceModeSettings } from 'ee/maintenance_mode_settings';
initMaintenanceModeSettings();
- return unless ::Gitlab::Geo.license_allows? && Feature.enabled?(:maintenance_mode)
%section.settings.no-animate#js-maintenance-mode-toggle{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
= _('Maintenance mode')
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Prevent users from performing write operations on GitLab while performing maintenance.')
.settings-content
#js-maintenance-mode-settings{ data: { "maintenance-enabled" => @application_setting.maintenance_mode,
"banner-message" => @application_setting.maintenance_mode_message } }
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlToggle, GlFormTextarea, GlForm, GlLoadingIcon } from '@gitlab/ui';
import MaintenanceModeSettingsApp from 'ee/maintenance_mode_settings/components/app.vue';
import { MOCK_BASIC_SETTINGS_DATA } from '../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('MaintenanceModeSettingsApp', () => {
let wrapper;
const actionSpies = {
updateMaintenanceModeSettings: jest.fn(),
setMaintenanceEnabled: jest.fn(),
setBannerMessage: jest.fn(),
};
const createComponent = initialState => {
const store = new Vuex.Store({
state: {
...MOCK_BASIC_SETTINGS_DATA,
...initialState,
},
actions: actionSpies,
});
wrapper = shallowMount(MaintenanceModeSettingsApp, {
localVue,
store,
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findMaintenanceModeSettingsForm = () => wrapper.find(GlForm);
const findGlLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findGlToggle = () => wrapper.find(GlToggle);
const findGlFormTextarea = () => wrapper.find(GlFormTextarea);
const findSubmitButton = () => findMaintenanceModeSettingsForm().find('[type="submit"]');
describe('template', () => {
describe('when loading is true', () => {
beforeEach(() => {
createComponent({ loading: true });
});
it('renders GlLoadingIcon', () => {
expect(findGlLoadingIcon().exists()).toBe(true);
});
it('does not render the MaintenanceModeSettingsForm', () => {
expect(findMaintenanceModeSettingsForm().exists()).toBe(false);
});
});
describe('when loading is false', () => {
beforeEach(() => {
createComponent();
});
it('does not render GlLoadingIcon', () => {
expect(findGlLoadingIcon().exists()).toBe(false);
});
it('renders the MaintenanceModeSettingsForm', () => {
expect(findMaintenanceModeSettingsForm().exists()).toBe(true);
});
it('renders the Submit Button', () => {
expect(findSubmitButton().exists()).toBe(true);
});
});
});
describe('GlToggle', () => {
describe('onChange', () => {
beforeEach(() => {
createComponent();
findGlToggle().vm.$emit('change', false);
});
it('calls setMaintenanceEnabled with the new boolean', () => {
expect(actionSpies.setMaintenanceEnabled).toHaveBeenCalledWith(expect.any(Object), {
maintenanceEnabled: false,
});
});
});
});
describe('GlFormTextarea', () => {
describe('onInput', () => {
beforeEach(() => {
createComponent();
findGlFormTextarea().vm.$emit('input', 'Hello World');
});
it('calls setBannerMessage with the new string', () => {
expect(actionSpies.setBannerMessage).toHaveBeenCalledWith(expect.any(Object), {
bannerMessage: 'Hello World',
});
});
});
});
describe('MaintenanceModeSettingsForm', () => {
describe('onSubmit', () => {
beforeEach(() => {
createComponent();
findMaintenanceModeSettingsForm().vm.$emit('submit', { preventDefault: () => {} });
});
it('calls updateMaintenanceModeSettings', () => {
expect(actionSpies.updateMaintenanceModeSettings).toHaveBeenCalledTimes(1);
});
});
});
});
import * as types from 'ee/maintenance_mode_settings/store/mutation_types';
export const MOCK_APPLICATION_SETTINGS_UPDATE_RESPONSE = {
maintenance_mode: true,
maintenance_mode_message: 'Test Message',
};
export const MOCK_BASIC_SETTINGS_DATA = {
maintenanceEnabled: MOCK_APPLICATION_SETTINGS_UPDATE_RESPONSE.maintenance_mode,
bannerMessage: MOCK_APPLICATION_SETTINGS_UPDATE_RESPONSE.maintenance_mode_message,
};
export const ACTIONS_TEST_DATA = {
setMaintenanceEnabledData: { maintenanceEnabled: MOCK_BASIC_SETTINGS_DATA.maintenanceEnabled },
setMaintenanceEnabledMutations: {
type: types.SET_MAINTENANCE_ENABLED,
payload: MOCK_BASIC_SETTINGS_DATA.maintenanceEnabled,
},
setBannerMessageData: { bannerMessage: MOCK_BASIC_SETTINGS_DATA.bannerMessage },
setBannerMessageMutations: {
type: types.SET_BANNER_MESSAGE,
payload: MOCK_BASIC_SETTINGS_DATA.bannerMessage,
},
successfulAxiosCall: {
method: 'onPut',
code: 200,
res: MOCK_APPLICATION_SETTINGS_UPDATE_RESPONSE,
},
updateSuccessMutations: [
{ type: types.REQUEST_UPDATE_MAINTENANCE_MODE_SETTINGS },
{
type: types.RECEIVE_UPDATE_MAINTENANCE_MODE_SETTINGS_SUCCESS,
payload: MOCK_BASIC_SETTINGS_DATA,
},
],
errorAxiosCall: { method: 'onPut', code: 500, res: null },
updateErrorMutations: [
{ type: types.REQUEST_UPDATE_MAINTENANCE_MODE_SETTINGS },
{ type: types.RECEIVE_UPDATE_MAINTENANCE_MODE_SETTINGS_ERROR },
],
};
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from 'ee/maintenance_mode_settings/store/actions';
import { createState } from 'ee/maintenance_mode_settings/store/state';
import {
DEFAULT_MAINTENANCE_ENABLED,
DEFAULT_BANNER_MESSAGE,
} from 'ee/maintenance_mode_settings/constants';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { ACTIONS_TEST_DATA } from '../mock_data';
jest.mock('~/flash');
describe('MaintenanceModeSettings Store Actions', () => {
let mock;
let state;
const noCallback = () => {};
const flashCallback = () => {
expect(createFlash).toHaveBeenCalledTimes(1);
createFlash.mockClear();
};
beforeEach(() => {
mock = new MockAdapter(axios);
state = createState({
maintenanceEnabled: DEFAULT_MAINTENANCE_ENABLED,
bannerMessage: DEFAULT_BANNER_MESSAGE,
});
});
afterEach(() => {
mock.restore();
state = null;
});
describe.each`
action | data | mutationCall
${actions.setMaintenanceEnabled} | ${ACTIONS_TEST_DATA.setMaintenanceEnabledData} | ${ACTIONS_TEST_DATA.setMaintenanceEnabledMutations}
${actions.setBannerMessage} | ${ACTIONS_TEST_DATA.setBannerMessageData} | ${ACTIONS_TEST_DATA.setBannerMessageMutations}
`(`non-axios calls`, ({ action, data, mutationCall }) => {
describe(action.name, () => {
it(`should commit mutation ${mutationCall.type}`, () => {
return testAction(action, data, state, [mutationCall], []).then(() => noCallback());
});
});
});
describe.each`
action | axiosMock | type | mutationCalls | callback
${actions.updateMaintenanceModeSettings} | ${ACTIONS_TEST_DATA.successfulAxiosCall} | ${'success'} | ${ACTIONS_TEST_DATA.updateSuccessMutations} | ${noCallback}
${actions.updateMaintenanceModeSettings} | ${ACTIONS_TEST_DATA.errorAxiosCall} | ${'error'} | ${ACTIONS_TEST_DATA.updateErrorMutations} | ${flashCallback}
`(`axios calls`, ({ action, axiosMock, type, mutationCalls, callback }) => {
describe(action.name, () => {
describe(`on ${type}`, () => {
beforeEach(() => {
mock[axiosMock.method]().replyOnce(axiosMock.code, axiosMock.res);
});
it(`should dispatch the correct mutations`, () => {
return testAction(action, null, state, mutationCalls, []).then(() => callback());
});
});
});
});
});
import {
DEFAULT_MAINTENANCE_ENABLED,
DEFAULT_BANNER_MESSAGE,
} from 'ee/maintenance_mode_settings/constants';
import * as types from 'ee/maintenance_mode_settings/store/mutation_types';
import mutations from 'ee/maintenance_mode_settings/store/mutations';
import { createState } from 'ee/maintenance_mode_settings/store/state';
import { MOCK_BASIC_SETTINGS_DATA } from '../mock_data';
describe('MaintenanceModeSettings Store Mutations', () => {
let state;
beforeEach(() => {
state = createState({
maintenanceEnabled: DEFAULT_MAINTENANCE_ENABLED,
bannerMessage: DEFAULT_BANNER_MESSAGE,
});
});
afterEach(() => {
state = null;
});
describe.each`
mutation | data | loadingBefore | loadingAfter
${types.REQUEST_UPDATE_MAINTENANCE_MODE_SETTINGS} | ${null} | ${false} | ${true}
${types.REQUEST_UPDATE_MAINTENANCE_MODE_SETTINGS} | ${null} | ${false} | ${true}
${types.RECEIVE_UPDATE_MAINTENANCE_MODE_SETTINGS_ERROR} | ${null} | ${true} | ${false}
`(`Loading Mutations: `, ({ mutation, data, loadingBefore, loadingAfter }) => {
describe(`${mutation}`, () => {
it(`sets loading to ${loadingAfter}`, () => {
state.loading = loadingBefore;
mutations[mutation](state, data);
expect(state.loading).toBe(loadingAfter);
});
});
});
describe('RECEIVE_UPDATE_MAINTENANCE_MODE_SETTINGS_SUCCESS', () => {
it('sets maintenanceEnabled and bannerMessage array with data', () => {
mutations[types.RECEIVE_UPDATE_MAINTENANCE_MODE_SETTINGS_SUCCESS](
state,
MOCK_BASIC_SETTINGS_DATA,
);
expect(state.maintenanceEnabled).toBe(MOCK_BASIC_SETTINGS_DATA.maintenanceEnabled);
expect(state.bannerMessage).toBe(MOCK_BASIC_SETTINGS_DATA.bannerMessage);
});
});
describe('RECEIVE_UPDATE_MAINTENANCE_MODE_SETTINGS_ERROR', () => {
beforeEach(() => {
state.maintenanceEnabled = MOCK_BASIC_SETTINGS_DATA.maintenanceEnabled;
state.bannerMessage = MOCK_BASIC_SETTINGS_DATA.bannerMessage;
});
it('resets maintenanceEnabled and bannerMessage array', () => {
mutations[types.RECEIVE_UPDATE_MAINTENANCE_MODE_SETTINGS_ERROR](state);
expect(state.maintenanceEnabled).toBe(DEFAULT_MAINTENANCE_ENABLED);
expect(state.bannerMessage).toBe(DEFAULT_BANNER_MESSAGE);
});
});
describe('SET_MAINTENANCE_ENABLED', () => {
it('sets data for field', () => {
mutations[types.SET_MAINTENANCE_ENABLED](state, true);
expect(state.maintenanceEnabled).toBe(true);
});
});
describe('SET_BANNER_MESSAGE', () => {
it('sets data for field', () => {
mutations[types.SET_BANNER_MESSAGE](state, 'test');
expect(state.bannerMessage).toBe('test');
});
});
});
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'admin/application_settings/general.html.haml' do
let_it_be(:user) { create(:admin) }
let_it_be(:app_settings) { build(:application_setting) }
before do
assign(:application_setting, app_settings)
allow(view).to receive(:current_user).and_return(user)
end
describe 'maintenance mode' do
let(:maintenance_mode_flag) { true }
let(:license_allows) { true }
before do
stub_feature_flags(maintenance_mode: maintenance_mode_flag)
allow(Gitlab::Geo).to receive(:license_allows?).and_return(license_allows)
render
end
context 'when license does not allow' do
let(:license_allows) { false }
it 'does not show the Maintenance mode section' do
expect(rendered).not_to have_css('#js-maintenance-mode-toggle')
end
end
context 'when maintenance_mode feature is enabled' do
it 'shows the Maintenance mode section' do
expect(rendered).to have_css('#js-maintenance-mode-toggle')
end
end
context 'when maintenance_mode feature is disabled' do
let(:maintenance_mode_flag) { false }
it 'does not show the Maintenance mode section' do
expect(rendered).not_to have_css('#js-maintenance-mode-toggle')
end
end
end
end
......@@ -12932,6 +12932,9 @@ msgstr ""
msgid "GitLab is obtaining a Let's Encrypt SSL certificate for this domain. This process can take some time. Please try again later."
msgstr ""
msgid "GitLab is undergoing maintenance and is operating in a read-only mode."
msgstr ""
msgid "GitLab member or Email address"
msgstr ""
......@@ -27891,6 +27894,9 @@ msgstr ""
msgid "There was an error updating the Geo Settings"
msgstr ""
msgid "There was an error updating the Maintenance Mode Settings"
msgstr ""
msgid "There was an error updating the dashboard, branch name is invalid."
msgstr ""
......
import { shallowMount } from '@vue/test-utils';
import { GlToggle, GlFormTextarea, GlButton } from '@gitlab/ui';
import MaintenanceModeSettingsApp from '~/maintenance_mode_settings/components/app.vue';
describe('MaintenanceModeSettingsApp', () => {
let wrapper;
const createComponent = () => {
wrapper = shallowMount(MaintenanceModeSettingsApp);
};
afterEach(() => {
wrapper.destroy();
});
const findMaintenanceModeSettingsContainer = () => wrapper.find('article');
const findGlToggle = () => wrapper.find(GlToggle);
const findGlFormTextarea = () => wrapper.find(GlFormTextarea);
const findGlButton = () => wrapper.find(GlButton);
describe('template', () => {
beforeEach(() => {
createComponent();
});
it('renders the Maintenance Mode Settings container', () => {
expect(findMaintenanceModeSettingsContainer().exists()).toBe(true);
});
it('renders the GlToggle', () => {
expect(findGlToggle().exists()).toBe(true);
});
it('renders the GlFormTextarea', () => {
expect(findGlFormTextarea().exists()).toBe(true);
});
it('renders the GlButton', () => {
expect(findGlButton().exists()).toBe(true);
});
});
});
......@@ -33,32 +33,4 @@ RSpec.describe 'admin/application_settings/general.html.haml' do
end
end
end
describe 'Maintenance mode' do
let(:maintenance_mode_flag) { true }
before do
assign(:application_setting, app_settings)
stub_feature_flags(maintenance_mode: maintenance_mode_flag)
allow(view).to receive(:current_user).and_return(user)
end
context 'when maintenance_mode feature is enabled' do
it 'show the Maintenance mode section' do
render
expect(rendered).to have_css('#js-maintenance-mode-toggle')
end
end
context 'when maintenance_mode feature is disabled' do
let(:maintenance_mode_flag) { false }
it 'hide the Maintenance mode section' do
render
expect(rendered).not_to have_css('#js-maintenance-mode-toggle')
end
end
end
end
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