Skip to content

Commit

Permalink
Implement Monaco editor for Alertmanager YAML editor
Browse files Browse the repository at this point in the history
  • Loading branch information
dtaylor113 committed Dec 4, 2019
1 parent 96ef6ea commit 245f36e
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 105 deletions.
48 changes: 36 additions & 12 deletions frontend/integration-tests/tests/monitoring.scenario.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { browser, ExpectedConditions as until } from 'protractor';

import { checkLogs, checkErrors, firstElementByTestID } from '../protractor.conf';
import * as crudView from '../views/crud.view';
import * as yamlView from '../views/yaml.view';
import * as monitoringView from '../views/monitoring.view';
import * as namespaceView from '../views/namespace.view';
import * as sidenavView from '../views/sidenav.view';
Expand Down Expand Up @@ -199,14 +200,14 @@ describe('Alertmanager: YAML', () => {
await firstElementByTestID('alertmanager').click();
await crudView.isLoaded();
await horizontalnavView.clickHorizontalTab('YAML');
await crudView.isLoaded();
expect(monitoringView.alertManagerYamlForm.isPresent()).toBe(true);
await yamlView.isLoaded();
expect(yamlView.yamlEditor.isPresent()).toBe(true);
});

it('saves Alertmanager YAML', async () => {
expect(monitoringView.successAlert.isPresent()).toBe(false);
await monitoringView.saveButton.click();
await crudView.isLoaded();
await yamlView.saveButton.click();
await yamlView.isLoaded();
expect(monitoringView.successAlert.isPresent()).toBe(true);
});
});
Expand Down Expand Up @@ -288,9 +289,7 @@ describe('Alertmanager: Configuration', () => {
await firstElementByTestID('label-value-0').sendKeys('warning');

await monitoringView.saveButton.click();

await crudView.isLoaded();

monitoringView.getFirstRowAsText().then((text) => {
expect(text).toEqual('MyReceiver pagerduty severity = warning');
});
Expand Down Expand Up @@ -359,19 +358,44 @@ describe('Alertmanager: Configuration', () => {
receivers:
- name: 'team-X-pager'
- name: 'team-DB-pager'`;

await crudView.isLoaded();
await horizontalnavView.clickHorizontalTab('YAML');
await crudView.isLoaded();
await firstElementByTestID('alert-manager-yaml-textarea').clear();
await firstElementByTestID('alert-manager-yaml-textarea').sendKeys(yaml);
await monitoringView.saveButton.click();
await crudView.isLoaded();
await yamlView.isLoaded();
await yamlView.setEditorContent(yaml);
await yamlView.saveButton.click();
await yamlView.isLoaded();
expect(monitoringView.successAlert.isPresent()).toBe(true);

await horizontalnavView.clickHorizontalTab('Overview');
await monitoringView.openFirstRowKebabMenu();
expect(monitoringView.disabledDeleteReceiverMenuItem.isPresent()).toBe(true);
expect(crudView.actionForLabel('Edit YAML').isPresent()).toBe(true); // should be 'Edit YAML' not 'Edit Receiver'
});

it('restores default/initial alertmanager.yaml', async () => {
// add receiver with sub-route
const defaultAlertmanagerYaml = `"global":
"resolve_timeout": "5m"
"receivers":
- "name": "null"
"route":
"group_by":
- "job"
"group_interval": "5m"
"group_wait": "30s"
"receiver": "null"
"repeat_interval": "12h"
"routes":
- "match":
"alertname": "Watchdog"
"receiver": "null"`;

await crudView.isLoaded();
await horizontalnavView.clickHorizontalTab('YAML');
await yamlView.isLoaded();
await yamlView.setEditorContent(defaultAlertmanagerYaml);
await yamlView.saveButton.click();
await yamlView.isLoaded();
expect(monitoringView.successAlert.isPresent()).toBe(true);
});
});
2 changes: 1 addition & 1 deletion frontend/integration-tests/views/monitoring.view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ export const saveButton = $('button[type=submit]');
export const modalConfirmButton = $('#confirm-action');

// YAML form
export const alertManagerYamlForm = $('.co-alert-manager-yaml__form');
export const successAlert = $('.pf-m-success');
export const helpText = $('.co-help-text');

Expand All @@ -39,6 +38,7 @@ export const alertRoutingEditButton = $('.co-alert-manager-config__edit-alert-ro
export const disabledDeleteReceiverMenuItem = $(
'.pf-c-dropdown__menu-item.pf-m-disabled[data-test-action="Delete Receiver"]',
);

const firstRow = element.all(by.css(`[data-test-rows="resource-row"]`)).first();

export const openFirstRowKebabMenu = () => {
Expand Down
1 change: 1 addition & 0 deletions frontend/integration-tests/views/yaml.view.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { $, $$, by, browser, ExpectedConditions as until } from 'protractor';
import { waitForNone } from '../protractor.conf';

export const yamlEditor = $('.yaml-editor');
export const saveButton = $('.yaml-editor__buttons').$('#save-changes');
export const cancelButton = $('.yaml-editor__buttons').element(by.buttonText('Cancel'));
export const isLoaded = () =>
Expand Down
68 changes: 39 additions & 29 deletions frontend/public/components/edit-yaml.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,14 @@ const EditYAML_ = connect(stateToProps)(
}

save() {
const { create, onSave } = this.props;
let obj;

if (!create && onSave) {
onSave(this.getEditor().getValue());
return;
}

try {
obj = safeLoad(this.getEditor().getValue());
} catch (e) {
Expand Down Expand Up @@ -666,7 +673,7 @@ const EditYAML_ = connect(stateToProps)(
});

const { error, success, stale, yaml, height, showSidebar } = this.state;
const { obj, download = true, header } = this.props;
const { obj, download = true, header, nonK8s = false, children: customAlerts } = this.props;
const readOnly = this.props.readOnly || this.state.notAllowed;
const options = { readOnly, scrollBeyondLastLine: false };
const model = this.getModel(obj);
Expand Down Expand Up @@ -704,32 +711,34 @@ const EditYAML_ = connect(stateToProps)(
<div className="co-p-has-sidebar__body">
<div className="yaml-editor" ref={(r) => (this.editor = r)}>
<div className="yaml-editor__links">
<div className="yaml-editor__link">
<Popover
aria-label="Shortcuts"
bodyContent={
<ShortcutTable>
<Shortcut ctrl keyName="space">
Activate auto complete
</Shortcut>
<Shortcut ctrlCmd shift keyName="o">
View document outline
</Shortcut>
<Shortcut hover>View property descriptions</Shortcut>
<Shortcut ctrlCmd keyName="s">
Save
</Shortcut>
</ShortcutTable>
}
maxWidth="25rem"
distance={18}
>
<Button type="button" variant="link" isInline>
<QuestionCircleIcon className="co-icon-space-r co-p-has-sidebar__sidebar-link-icon" />
View shortcuts
</Button>
</Popover>
</div>
{!nonK8s && (
<div className="yaml-editor__link">
<Popover
aria-label="Shortcuts"
bodyContent={
<ShortcutTable>
<Shortcut ctrl keyName="space">
Activate auto complete
</Shortcut>
<Shortcut ctrlCmd shift keyName="o">
View document outline
</Shortcut>
<Shortcut hover>View property descriptions</Shortcut>
<Shortcut ctrlCmd keyName="s">
Save
</Shortcut>
</ShortcutTable>
}
maxWidth="25rem"
distance={18}
>
<Button type="button" variant="link" isInline>
<QuestionCircleIcon className="co-icon-space-r co-p-has-sidebar__sidebar-link-icon" />
View shortcuts
</Button>
</Popover>
</div>
)}
{!showSidebar && hasSidebarContent && (
<>
<div className="co-action-divider--spaced">|</div>
Expand Down Expand Up @@ -757,6 +766,7 @@ const EditYAML_ = connect(stateToProps)(
onChange={(newValue) => this.setState({ yaml: newValue })}
/>
<div className="yaml-editor__buttons" ref={(r) => (this.buttons = r)}>
{customAlerts}
{error && (
<Alert
isInline
Expand All @@ -770,7 +780,7 @@ const EditYAML_ = connect(stateToProps)(
{success && (
<Alert isInline className="co-alert" variant="success" title={success} />
)}
{stale && (
{stale && !nonK8s && (
<Alert
isInline
className="co-alert"
Expand Down Expand Up @@ -801,7 +811,7 @@ const EditYAML_ = connect(stateToProps)(
Save
</Button>
)}
{!create && (
{!create && !nonK8s && (
<Button
type="submit"
variant="secondary"
Expand Down
12 changes: 0 additions & 12 deletions frontend/public/components/monitoring/_monitoring.scss
Original file line number Diff line number Diff line change
Expand Up @@ -232,18 +232,6 @@ $tooltip-background-color: #151515;
z-index: 1;
}

.co-alert-manager-yaml__explanation {
margin-bottom: 0;
}

.co-alert-manager-yaml__form-entry-wrapper {
position: relative;
}

.co-alert-manager-yaml__form co-file-dropzone__textarea {
min-height: 400px;
}

.co-alert-manager-config__edit-alert-routing-btn {
margin-bottom: 10px;
}
9 changes: 9 additions & 0 deletions frontend/public/components/monitoring/alert-manager-utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,15 @@ export const patchAlertManagerConfig = (
secret: K8sResourceKind,
yaml: object | string,
): Promise<any> => {
if (_.isString(yaml)) {
try {
safeLoad(yaml);
} catch (e) {
return new Promise(() => {
throw new Error(`Error parsing Alertmanager YAML: ${e}`);
});
}
}
const yamlString = _.isObject(yaml) ? safeDump(yaml) : yaml;
const yamlEncodedString = Base64.encode(yamlString);
const patch = [{ op: 'replace', path: '/data/alertmanager.yaml', value: yamlEncodedString }];
Expand Down
85 changes: 34 additions & 51 deletions frontend/public/components/monitoring/alert-manager-yaml-editor.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,24 @@
import * as React from 'react';
import * as _ from 'lodash-es';
import { ActionGroup, Button } from '@patternfly/react-core';

import { Alert } from '@patternfly/react-core';
import { Base64 } from 'js-base64';

import { K8sResourceKind } from '../../module/k8s';
import { ButtonBar, history, StatusBox } from '../utils';
import { StatusBox } from '../utils';
import { AsyncComponent } from '../utils/async';
import { patchAlertManagerConfig } from './alert-manager-utils';
import { Helmet } from 'react-helmet';

const DroppableFileInput = (props) => (
const EditAlertmanagerYAML = (props) => (
<AsyncComponent
loader={() => import('../utils/file-input').then((c) => c.DroppableFileInput)}
{...props}
loader={() => import('../edit-yaml').then((c) => c.EditYAML)}
create={false}
nonK8s={true}
/>
);

const AlertManagerYAMLEditor: React.FC<AlertManagerYAMLEditorProps> = ({
obj,
onCancel = history.goBack,
}) => {
const AlertManagerYAMLEditor: React.FC<AlertManagerYAMLEditorProps> = ({ obj }) => {
const secret: K8sResourceKind = obj;
const encodedAlertManagerYaml = _.get(secret, ['data', 'alertmanager.yaml']);
const initErrorMsg = _.isEmpty(encodedAlertManagerYaml)
Expand All @@ -28,70 +27,54 @@ const AlertManagerYAMLEditor: React.FC<AlertManagerYAMLEditorProps> = ({

const [errorMsg, setErrorMsg] = React.useState(initErrorMsg);
const [successMsg, setSuccessMsg] = React.useState();
const [inProgress, setInProgress] = React.useState(false);
const [alertManagerYamlStr, setAlertManagerYamlStr] = React.useState(
!_.isEmpty(encodedAlertManagerYaml) ? Base64.decode(encodedAlertManagerYaml) : '',
);
const alertManagerYamlStr = !_.isEmpty(encodedAlertManagerYaml)
? Base64.decode(encodedAlertManagerYaml)
: '';

const save = (e) => {
e.preventDefault();
if (_.isEmpty(alertManagerYamlStr)) {
const save = (yaml: string) => {
if (_.isEmpty(yaml)) {
setErrorMsg('Alertmanager configuration cannot be empty.');
setSuccessMsg('');
return;
}
setInProgress(true);
patchAlertManagerConfig(secret, alertManagerYamlStr).then(
patchAlertManagerConfig(secret, yaml).then(
(newSecret) => {
setSuccessMsg(
`${newSecret.metadata.name} has been updated to version ${
newSecret.metadata.resourceVersion
}`,
);
setErrorMsg('');
setInProgress(false);
},
(err) => {
setErrorMsg(err.message);
setSuccessMsg('');
setInProgress(false);
},
);
};

return (
<div className="co-m-pane__body">
<form
className="co-m-pane__body-group co-alert-manager-yaml-form co-m-pane__form"
onSubmit={save}
>
<p className="co-alert-manager-yaml__explanation">
Update this YAML to configure Routes, Receivers, Groupings and other Alertmanager settings
<>
<div className="co-m-nav-title">
<p className="help-block">
Update this YAML to configure Routes, Receivers, Groupings and other Alertmanager
settings.
</p>
<div className="co-alert-manager-yaml__form-entry-wrapper">
<div className="co-alert-manager-yaml__form">
<div className="form-group">
<DroppableFileInput
data-test-id="alert-manager-yaml-textarea"
onChange={setAlertManagerYamlStr}
inputFileData={alertManagerYamlStr}
inputFieldHelpText="Drag and drop file with your value here or browse to upload it."
/>
</div>
</div>
</div>
<ButtonBar errorMessage={errorMsg} successMessage={successMsg} inProgress={inProgress}>
<ActionGroup className="pf-c-form">
<Button type="submit" variant="primary" id="save-changes">
Save
</Button>
<Button type="button" variant="secondary" id="cancel" onClick={onCancel}>
Cancel
</Button>
</ActionGroup>
</ButtonBar>
</form>
</div>
</div>
<EditAlertmanagerYAML onSave={save} obj={alertManagerYamlStr}>
{errorMsg && (
<Alert
isInline
className="co-alert co-alert--scrollable"
variant="danger"
title="An error occurred"
>
<div className="co-pre-line">{errorMsg}</div>
</Alert>
)}
{successMsg && <Alert isInline className="co-alert" variant="success" title={successMsg} />}
</EditAlertmanagerYAML>
</>
);
};

Expand Down

0 comments on commit 245f36e

Please sign in to comment.