Commit 4886f07c authored by Olena Horal-Koretska's avatar Olena Horal-Koretska Committed by Natalia Tepluhina

Status page settings

parent 535d5da7
...@@ -7,5 +7,7 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -7,5 +7,7 @@ document.addEventListener('DOMContentLoaded', () => {
mountErrorTrackingForm(); mountErrorTrackingForm();
mountOperationSettings(); mountOperationSettings();
mountGrafanaIntegration(); mountGrafanaIntegration();
if (!IS_EE) {
initSettingsPanels(); initSettingsPanels();
}
}); });
...@@ -8,3 +8,4 @@ ...@@ -8,3 +8,4 @@
= render 'projects/settings/operations/external_dashboard' = render 'projects/settings/operations/external_dashboard'
= render 'projects/settings/operations/grafana_integration' = render 'projects/settings/operations/grafana_integration'
= render_if_exists 'projects/settings/operations/tracing' = render_if_exists 'projects/settings/operations/tracing'
= render_if_exists 'projects/settings/operations/status_page'
import '~/pages/projects/settings/operations/show/index';
import mountStatusPageForm from 'ee/status_page_settings';
import initSettingsPanels from '~/settings_panels';
document.addEventListener('DOMContentLoaded', () => {
mountStatusPageForm();
initSettingsPanels();
});
<script>
import { mapActions, mapState } from 'vuex';
import { mapComputed } from '~/vuex_shared/bindings';
import {
GlButton,
GlSprintf,
GlLink,
GlIcon,
GlFormGroup,
GlFormInput,
GlFormCheckbox,
} from '@gitlab/ui';
import { __, s__ } from '~/locale';
export default {
components: { GlButton, GlSprintf, GlLink, GlFormGroup, GlFormInput, GlIcon, GlFormCheckbox },
i18n: {
headerText: s__('StatusPage|Status page'),
expandBtnLabel: __('Expand'),
saveBtnLabel: __('Save changes'),
subHeaderText: s__(
'StatusPage|Configure file storage settings to link issues in this project to an external status page.',
),
introText: s__(
'StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}',
),
introLinkText: s__('StatusPage|your status page frontend.'),
activeLabel: s__('StatusPage|Active'),
bucket: {
label: s__('StatusPage|S3 Bucket name'),
helpText: s__('StatusPage|Bucket %{docsLink}'),
linkText: s__('StatusPage|configuration documentation'),
},
region: {
label: s__('StatusPage|AWS region'),
helpText: s__('StatusPage|For help with configuration, visit %{docsLink}'),
linkText: s__('StatusPage|AWS documentation'),
},
accessKey: {
label: s__('StatusPage|AWS access key ID'),
},
secretAccessKey: {
label: s__('StatusPage|AWS Secret access key'),
},
},
computed: {
...mapState(['loading']),
...mapComputed([
{ key: 'enabled', updateFn: 'setStatusPageEnabled' },
{ key: 'bucketName', updateFn: 'setStatusPageBucketName' },
{ key: 'region', updateFn: 'setStatusPageRegion' },
{ key: 'awsAccessKey', updateFn: 'setStatusPageAccessKey' },
{ key: 'awsSecretKey', updateFn: 'setStatusPageSecretAccessKey' },
]),
},
methods: {
...mapActions(['updateStatusPageSettings']),
},
};
</script>
<template>
<section id="status-page" class="settings no-animate js-status-page-settings">
<div class="settings-header">
<h3 ref="sectionHeader" class="h4">
{{ $options.i18n.headerText }}
</h3>
<gl-button ref="toggleBtn" class="js-settings-toggle">{{
$options.i18n.expandBtnLabel
}}</gl-button>
<p ref="sectionSubHeader">
{{ $options.i18n.subHeaderText }}
</p>
</div>
<div class="settings-content">
<!-- eslint-disable @gitlab/vue-i18n/no-bare-attribute-strings -->
<p>
<gl-sprintf :message="$options.i18n.introText">
<template #docsLink>
<gl-link href="#">
<span>{{ $options.i18n.introLinkText }}</span>
</gl-link>
</template>
</gl-sprintf>
</p>
<form ref="settingsForm" @submit.prevent="updateStatusPageSettings">
<gl-form-group class="gl-pl-0 mb-3">
<gl-form-checkbox v-model="enabled">
<span class="bold">{{ $options.i18n.activeLabel }}</span></gl-form-checkbox
>
</gl-form-group>
<gl-form-group
:label="$options.i18n.bucket.label"
label-size="sm"
label-for="status-page-s3-bucket-name"
class="col-8 col-md-9 gl-pl-0 mb-3"
>
<gl-form-input id="status-page-s3-bucket-name" v-model="bucketName" />
<p class="form-text text-muted">
<gl-sprintf :message="$options.i18n.bucket.helpText">
<template #docsLink>
<gl-link
target="_blank"
href="https://docs.aws.amazon.com/AmazonS3/latest/dev/HostingWebsiteOnS3Setup.html"
>
<span>{{ $options.i18n.bucket.linkText }}</span>
<gl-icon name="external-link" class="vertical-align-middle" />
</gl-link>
</template>
</gl-sprintf>
</p>
</gl-form-group>
<gl-form-group
:label="$options.i18n.region.label"
label-size="sm"
label-for="status-page-aws-region"
class="col-8 col-md-9 gl-pl-0 mb-3"
>
<gl-form-input
id="status-page-aws-region"
v-model="region"
placeholder="example: us-west-2"
/>
<p class="form-text text-muted">
<gl-sprintf :message="$options.i18n.region.helpText">
<template #docsLink>
<gl-link href="https://github.com/aws/aws-sdk-ruby#configuration" target="_blank">
<span>{{ $options.i18n.region.linkText }}</span>
<gl-icon name="external-link" class="vertical-align-middle" />
</gl-link>
</template>
</gl-sprintf>
</p>
</gl-form-group>
<gl-form-group
:label="$options.i18n.accessKey.label"
label-size="sm"
label-for="status-page-aws-access-key-id"
class="col-8 col-md-9 gl-pl-0 mb-3"
>
<gl-form-input id="status-page-aws-access-key " v-model="awsAccessKey" />
</gl-form-group>
<gl-form-group
:label="$options.i18n.secretAccessKey.label"
label-size="sm"
label-for="status-page-aws-secret-access-key"
class="col-8 col-md-9 gl-pl-0 mb-3"
>
<gl-form-input id="status-page-aws-secret-access-key " v-model="awsSecretKey" />
</gl-form-group>
<gl-button
ref="submitBtn"
:disabled="loading"
variant="success"
type="submit"
class="js-no-auto-disable"
>
{{ $options.i18n.saveBtnLabel }}
</gl-button>
</form>
</div>
</section>
</template>
import Vue from 'vue';
import StatusPageSettings from './components/settings_form.vue';
import createStore from './store';
export default () => {
const el = document.querySelector('.js-status-page-settings');
if (!el) {
return null;
}
return new Vue({
el,
store: createStore(el.dataset),
render(createElement) {
return createElement(StatusPageSettings);
},
});
};
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import createFlash from '~/flash';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import * as mutationTypes from './mutation_types';
export const setStatusPageEnabled = ({ commit }, { enabled }) =>
commit(mutationTypes.SET_STATUS_PAGE_ENABLED, enabled);
export const setStatusPageBucketName = ({ commit }, { bucketName }) =>
commit(mutationTypes.SET_BUCKET_NAME, bucketName);
export const setStatusPageRegion = ({ commit }, { region }) =>
commit(mutationTypes.SET_REGION, region);
export const setStatusPageAccessKey = ({ commit }, { awsAccessKey }) =>
commit(mutationTypes.SET_ACCESS_KEY_ID, awsAccessKey);
export const setStatusPageSecretAccessKey = ({ commit }, { awsSecretKey }) =>
commit(mutationTypes.SET_SECRET_ACCESS_KEY, awsSecretKey);
export const updateStatusPageSettings = ({ state, dispatch, commit }) => {
commit(mutationTypes.LOADING, true);
axios
.patch(state.operationsSettingsEndpoint, {
project: {
status_page_setting_attributes: {
enabled: state.enabled,
aws_s3_bucket_name: state.bucketName,
aws_region: state.region,
aws_access_key: state.awsAccessKey,
aws_secret_key: state.awsSecretKey,
},
},
})
.then(() => dispatch('receiveStatusPageSettingsUpdateSuccess'))
.catch(error => dispatch('receiveStatusPageSettingsUpdateError', error))
.finally(() => commit(mutationTypes.LOADING, false));
};
export const receiveStatusPageSettingsUpdateSuccess = () => {
/**
* The operations_controller currently handles successful requests
* by creating a flash banner messsage to notify the user.
*/
refreshCurrentPage();
};
export const receiveStatusPageSettingsUpdateError = (_, error) => {
const { response } = error;
const message = response?.data?.message || '';
createFlash(`${__('There was an error saving your changes.')} ${message}`, 'alert');
};
import Vue from 'vue';
import Vuex from 'vuex';
import createState from './state';
import * as actions from './actions';
import mutations from './mutations';
Vue.use(Vuex);
export default initialState =>
new Vuex.Store({
state: createState(initialState),
actions,
mutations,
});
export const SET_STATUS_PAGE_ENABLED = 'SET_STATUS_PAGE_ENABLED';
export const SET_BUCKET_NAME = 'SET_BUCKET_NAME';
export const SET_REGION = 'SET_REGION';
export const SET_ACCESS_KEY_ID = 'SET_ACCESS_KEY_ID';
export const SET_SECRET_ACCESS_KEY = 'SET_SECRET_ACCESS_KEY';
export const LOADING = 'LOADING';
import * as types from './mutation_types';
export default {
[types.SET_STATUS_PAGE_ENABLED](state, enabled) {
state.enabled = enabled;
},
[types.SET_BUCKET_NAME](state, bucketName) {
state.bucketName = bucketName;
},
[types.SET_REGION](state, region) {
state.region = region;
},
[types.SET_ACCESS_KEY_ID](state, awsAccessKey) {
state.awsAccessKey = awsAccessKey;
},
[types.SET_SECRET_ACCESS_KEY](state, awsSecretKey) {
state.awsSecretKey = awsSecretKey;
},
[types.LOADING](state, loading) {
state.loading = loading;
},
};
import { parseBoolean } from '~/lib/utils/common_utils';
export default (initialState = {}) => ({
enabled: parseBoolean(initialState.enabled) || false,
bucketName: initialState.bucketName || '',
region: initialState.region || '',
awsAccessKey: initialState.awsAccessKey || '',
awsSecretKey: initialState.awsSecretKey || '',
operationsSettingsEndpoint: initialState.operationsSettingsEndpoint || '',
loading: false,
});
- return unless @project.feature_available?(:status_page, current_user)
- setting = status_page_settings_data(@project.status_page_setting)
.js-status-page-settings{ data: { operations_settings_endpoint: project_settings_operations_path(@project),
enabled: setting['setting-enabled'],
bucket_name: setting['setting-aws-s3-bucket-name'],
region: setting['setting-aws-region'],
aws_access_key: setting['setting-aws-access-key'],
aws_secret_key: setting['setting-masked-aws-secret-key'] } }
---
title: Status page settings
merge_request: 25820
author:
type: added
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Status Page settings form default state should match the default snapshot 1`] = `
<section
class="settings no-animate js-status-page-settings"
id="status-page"
>
<div
class="settings-header"
>
<h3
class="h4"
>
Status page
</h3>
<gl-button-stub
class="js-settings-toggle"
size="md"
variant="secondary"
>
Expand
</gl-button-stub>
<p>
Configure file storage settings to link issues in this project to an external status page.
</p>
</div>
<div
class="settings-content"
>
<p>
<gl-sprintf-stub
message="To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
/>
</p>
<form>
<gl-form-group-stub
class="gl-pl-0 mb-3"
>
<gl-form-checkbox-stub>
<span
class="bold"
>
Active
</span>
</gl-form-checkbox-stub>
</gl-form-group-stub>
<gl-form-group-stub
class="col-8 col-md-9 gl-pl-0 mb-3"
label="S3 Bucket name"
label-for="status-page-s3-bucket-name"
label-size="sm"
>
<gl-form-input-stub
id="status-page-s3-bucket-name"
value=""
/>
<p
class="form-text text-muted"
>
<gl-sprintf-stub
message="Bucket %{docsLink}"
/>
</p>
</gl-form-group-stub>
<gl-form-group-stub
class="col-8 col-md-9 gl-pl-0 mb-3"
label="AWS region"
label-for="status-page-aws-region"
label-size="sm"
>
<gl-form-input-stub
id="status-page-aws-region"
placeholder="example: us-west-2"
value=""
/>
<p
class="form-text text-muted"
>
<gl-sprintf-stub
message="For help with configuration, visit %{docsLink}"
/>
</p>
</gl-form-group-stub>
<gl-form-group-stub
class="col-8 col-md-9 gl-pl-0 mb-3"
label="AWS access key ID"
label-for="status-page-aws-access-key-id"
label-size="sm"
>
<gl-form-input-stub
id="status-page-aws-access-key "
value=""
/>
</gl-form-group-stub>
<gl-form-group-stub
class="col-8 col-md-9 gl-pl-0 mb-3"
label="AWS Secret access key"
label-for="status-page-aws-secret-access-key"
label-size="sm"
>
<gl-form-input-stub
id="status-page-aws-secret-access-key "
value=""
/>
</gl-form-group-stub>
<gl-button-stub
class="js-no-auto-disable"
size="md"
type="submit"
variant="success"
>
Save changes
</gl-button-stub>
</form>
</div>
</section>
`;
import { shallowMount } from '@vue/test-utils';
import StatusPageSettingsForm from 'ee/status_page_settings/components/settings_form.vue';
import createStore from 'ee/status_page_settings/store';
describe('Status Page settings form', () => {
let wrapper;
const store = createStore();
const findForm = () => wrapper.find({ ref: 'settingsForm' });
const findToggleButton = () => wrapper.find({ ref: 'toggleBtn' });
const findSectionHeader = () => wrapper.find({ ref: 'sectionHeader' });
const findSectionSubHeader = () => wrapper.find({ ref: 'sectionSubHeader' });
beforeEach(() => {
wrapper = shallowMount(StatusPageSettingsForm, { store });
});
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
});
describe('default state', () => {
it('should match the default snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
});
it('renders header text', () => {
expect(findSectionHeader().text()).toBe('Status page');
});
describe('expand/collapse button', () => {
it('renders as an expand button by default', () => {
expect(findToggleButton().text()).toBe('Expand');
});
});
describe('sub-header', () => {
it('renders descriptive text', () => {
expect(findSectionSubHeader().text()).toContain(
'Configure file storage settings to link issues in this project to an external status page.',
);
});
});
describe('form', () => {
beforeEach(() => {
jest.spyOn(wrapper.vm, 'updateStatusPageSettings').mockImplementation();
});
describe('submit button', () => {
it('submits form on click', () => {
findForm().trigger('submit');
expect(wrapper.vm.updateStatusPageSettings).toHaveBeenCalled();
});
});
});
});
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import * as actions from 'ee/status_page_settings/store/actions';
import * as types from 'ee/status_page_settings/store/mutation_types';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
jest.mock('~/flash.js');
jest.mock('~/lib/utils/url_utility');
let mock;
describe('Status Page actions', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
createFlash.mockClear();
});
const state = {
enabled: true,
bucketName: 'test-bucket',
region: 'us-west',
};
it.each`
mutation | action | value | key
${types.SET_STATUS_PAGE_ENABLED} | ${'setStatusPageEnabled'} | ${true} | ${'enabled'}
${types.SET_BUCKET_NAME} | ${'setStatusPageBucketName'} | ${'my-bucket'} | ${'bucketName'}
${types.SET_REGION} | ${'setStatusPageRegion'} | ${'us-west'} | ${'region'}
${types.SET_ACCESS_KEY_ID} | ${'setStatusPageAccessKey'} | ${'key-id'} | ${'awsAccessKey'}
${types.SET_SECRET_ACCESS_KEY} | ${'setStatusPageSecretAccessKey'} | ${'secret'} | ${'awsSecretKey'}
`('$action will commit $mutation with $value', ({ mutation, action, value, key }) => {
testAction(
actions[action],
{ [key]: value },
null,
[
{
type: mutation,
payload: value,
},
],
[],
);
});
describe('updateStatusPageSettings', () => {
it('should handle successful status update', () => {
mock.onPatch().reply(200, {});
testAction(
actions.updateStatusPageSettings,
null,
state,
[
{
payload: true,
type: types.LOADING,
},
{
payload: false,
type: types.LOADING,
},
],
[{ type: 'receiveStatusPageSettingsUpdateSuccess' }],
);
});
it('should handle unsuccessful status update', () => {
mock.onPatch().reply(400, {});
testAction(
actions.updateStatusPageSettings,
null,
state,
[
{
payload: true,
type: types.LOADING,
},
{
payload: false,
type: types.LOADING,
},
],
[
{
payload: expect.any(Object),
type: 'receiveStatusPageSettingsUpdateError',
},
],
);
});
});
describe('receiveStatusPageSettingsUpdateSuccess', () => {
it('should handle successful settings update', done => {
testAction(actions.receiveStatusPageSettingsUpdateSuccess, null, null, [], [], () => {
expect(refreshCurrentPage).toHaveBeenCalledTimes(1);
done();
});
});
});
describe('receiveStatusPageSettingsUpdateError', () => {
const error = { response: { data: { message: 'Update error' } } };
it('should handle error update', done => {
testAction(actions.receiveStatusPageSettingsUpdateError, error, null, [], [], () => {
expect(createFlash).toHaveBeenCalledWith(
`There was an error saving your changes. ${error.response.data.message}`,
'alert',
);
done();
});
});
});
});
...@@ -18771,6 +18771,45 @@ msgstr "" ...@@ -18771,6 +18771,45 @@ msgstr ""
msgid "Status: %{title}" msgid "Status: %{title}"
msgstr "" msgstr ""
msgid "StatusPage|AWS Secret access key"
msgstr ""
msgid "StatusPage|AWS access key ID"
msgstr ""
msgid "StatusPage|AWS documentation"
msgstr ""
msgid "StatusPage|AWS region"
msgstr ""
msgid "StatusPage|Active"
msgstr ""
msgid "StatusPage|Bucket %{docsLink}"
msgstr ""
msgid "StatusPage|Configure file storage settings to link issues in this project to an external status page."
msgstr ""
msgid "StatusPage|For help with configuration, visit %{docsLink}"
msgstr ""
msgid "StatusPage|S3 Bucket name"
msgstr ""
msgid "StatusPage|Status page"
msgstr ""
msgid "StatusPage|To publish incidents to an external status page, GitLab will store a JSON file in your Amazon S3 account in a location accessible to your external status page service. Make sure to also set up %{docsLink}"
msgstr ""
msgid "StatusPage|configuration documentation"
msgstr ""
msgid "StatusPage|your status page frontend."
msgstr ""
msgid "Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments." msgid "Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments."
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