Skip to content

Commit

Permalink
created custom setup view
Browse files Browse the repository at this point in the history
  • Loading branch information
ziegfried committed Jun 16, 2020
1 parent 02aa702 commit 0759d1d
Show file tree
Hide file tree
Showing 14 changed files with 4,145 additions and 76 deletions.
3 changes: 3 additions & 0 deletions .babelrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module.exports = {
presets: ['@splunk/babel-preset'],
};
21 changes: 20 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,32 @@
"repository": "[email protected]:ziegfried/splunk-slack-alerts.git",
"author": "Siegfried Puchbauer <[email protected]>",
"license": "Apache-2.0",
"dependencies": {
"@splunk/react-page": "^3.0.0",
"@splunk/react-toast-notifications": "^0.7.0",
"@splunk/react-ui": "^2",
"react": "^16",
"react-dom": "^16",
"styled-components": "^4"
},
"devDependencies": {
"@babel/core": "^7",
"@splunk/babel-preset": "^3.0.0",
"@splunk/webpack-configs": "^5.0.0",
"babel-loader": "^8.0.4",
"onchange": "^3.2.1",
"splunk-slap": "^0.0.6"
"prettier": "^2.0.5",
"splunk-slap": "^0.0.6",
"webpack": "^4.16.2",
"webpack-cli": "^3.1.0",
"webpack-livereload-plugin": "^2.1.1",
"webpack-merge": "^4.1.3"
},
"scripts": {
"setup": "yarn install && yarn build && yarn symlink",
"build": "slap stage",
"build:pages": "webpack --config=src/ui/webpack.config.js --bail",
"slap:post-stage": "yarn build:pages",
"symlink": "slap symlink",
"package": "slap package --prod",
"pkg": "yarn package",
Expand Down
23 changes: 23 additions & 0 deletions src/app/appserver/templates/setup.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!doctype html>
<html class="no-js" lang="">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<title>
Slack Alerts Setup
</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="apple-touch-icon" href="apple-touch-icon.png">
</head>
<body>
<script src="${make_url('/config?autoload=1')}" crossorigin="use-credentials"></script>
<script src="${make_url('/static/js/i18n.js')}"></script>
<script src="${make_url('/i18ncatalog?autoload=1')}"></script>
<%
page_path = "/static/app/slack_alerts/pages/slack_alerts_setup.js"
common_path = "/static/app/slack_alerts/pages/common.js"
%>
<script src="${make_url(common_path)}"></script>
<script src="${make_url(page_path)}"></script>
</body>
</html>
1 change: 1 addition & 0 deletions src/app/default/app.conf
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[ui]
is_visible = 0
label = Slack Alerts
setup_view = slack_alerts_setup

[launcher]
author = Siegfried Puchbauer
Expand Down
4 changes: 4 additions & 0 deletions src/app/default/data/ui/views/slack_alerts_setup.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0"?>
<view template="slack_alerts:/templates/setup.html" type="html">
<label>Slack Alerts Setup</label>
</view>
41 changes: 0 additions & 41 deletions src/app/default/setup.xml

This file was deleted.

122 changes: 122 additions & 0 deletions src/ui/pages/slack_alerts_setup/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import { useState, useEffect, useCallback } from 'react';
import { makeUrl } from './utils';
import Toaster, { makeCreateToast } from '@splunk/react-toast-notifications/Toaster';

const createToast = makeCreateToast(Toaster);

function getFormKey() {
const prefix = `splunkweb_csrf_token_${window.$C.MRSPARKLE_PORT_NUMBER}=`;
if (document.cookie) {
for (const chunk of document.cookie.split(';')) {
const cookie = String(chunk).trim();
if (cookie.startsWith(prefix)) {
return decodeURIComponent(cookie.slice(prefix.length));
}
}
}
}

export function loadConfig() {
return fetch(
makeUrl('/splunkd/__raw/servicesNS/-/-/alerts/alert_actions/slack?output_mode=json&_=' + Date.now())
)
.then((res) => {
if (!res.ok) {
throw new Error(`Failed to fetch config: HTTP status ${res.status}`);
}
return res.json();
})
.then((data) => {
const d = data.entry[0].content;
return {
webhook_url: d['param.webhook_url'],
from_user: d['param.from_user'],
from_user_icon: d['param.from_user_icon'],
};
});
}

export function updateConfig(data) {
return fetch(
makeUrl(`/splunkd/__raw/servicesNS/-/slack_alerts/alerts/alert_actions/slack?output_mode=json`),
{
method: 'POST',
body: [
`param.webhook_url=${encodeURIComponent(data.webhook_url)}`,
`param.from_user=${encodeURIComponent(data.from_user)}`,
`param.from_user_icon=${encodeURIComponent(data.from_user_icon)}`,
].join('&'),
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest',
'X-Splunk-Form-Key': getFormKey(),
},
}
).then((res) => {
if (!res.ok) {
throw new Error(`Failed to save: HTTP status ${res.status}`);
}
});
}

const eq = (a, b) => {
const ka = Object.keys(a);
const kb = Object.keys(b);
return ka.length === kb.length && ka.every((k) => k in b && a[k] === b[k]);
};

export function useSlackConfig() {
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [savedData, setSavedData] = useState({});
const [data, setData] = useState({});

useEffect(() => {
loadConfig().then(
(cfg) => {
setSavedData(cfg);
setData(cfg);
setLoading(false);
},
(e) => {
setError(`Error: ${e.message}`);
setLoading(false);
}
);
}, [setLoading, setError]);

const update = useCallback(
(data) => {
setData(data);
},
[setData]
);

const save = useCallback(() => {
if (!eq(savedData, data)) {
updateConfig(data).then(
() => {
setSavedData(data);
createToast({
message: 'Successfully updated Slack alert action',
type: 'success',
});
},
(e) => {
setError(`Error: ${e.message}`);
createToast({
message: `Failed to save changes: ${e.message}`,
type: 'error',
autoDismiss: false,
});
}
);
}
}, [data, savedData, setSavedData, setError]);

return [{ loading, error, data, isDirty: !eq(savedData, data) }, update, save];
}

export function redirectToAlertListingPage() {
window.location = makeUrl('/manager/slack_alerts/alert_actions');
}
84 changes: 84 additions & 0 deletions src/ui/pages/slack_alerts_setup/form.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import Button from '@splunk/react-ui/Button';
import ControlGroup from '@splunk/react-ui/ControlGroup';
import Heading from '@splunk/react-ui/Heading';
import Link from '@splunk/react-ui/Link';
import Paragraph from '@splunk/react-ui/Paragraph';
import Text from '@splunk/react-ui/Text';
import React, { useCallback } from 'react';
import { useSlackConfig, redirectToAlertListingPage } from './config';
import { openSlackMessagePreview } from './preview';
import { ButtonGroup, FormWrapper } from './styles';

export function SetupForm() {
const [{ loading, error, data, isDirty }, update, save] = useSlackConfig();

const updateUrl = useCallback((e, { value }) => update({ ...data, webhook_url: value }));
const updateUser = useCallback((e, { value }) => update({ ...data, from_user: value }));
const updateUserIcon = useCallback((e, { value }) => update({ ...data, from_user_icon: value }));

const webhookUrl = loading ? '' : data.webhook_url;
const fromUserName = loading ? '' : data.from_user;
const fromUserIcon = loading ? '' : data.from_user_icon;

return (
<>
<Heading level={3}>Slack Incoming Webhook</Heading>
<Paragraph>
This alert action uses Slack's{' '}
<Link to="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks" openInNewContext>
Incoming Webhooks
</Link>{' '}
to post messages from Splunk into Slack channels. You can set a default webhook URL here,
which will be used for all alerts be default. Each alert can override and use a different
webhook URL that has different permissions or can send to a different Slack workspace.
</Paragraph>
<FormWrapper>
<ControlGroup
label="Webhook URL"
help={
<Link to="https://slack.com/apps/A0F7XDUAZ-incoming-webhooks" openInNewContext>
Configure Slack incoming webhook
</Link>
}
>
<Text
value={webhookUrl}
onChange={updateUrl}
disabled={loading}
placeholder="https://hooks.slack.com/services/XXXXX/YYYY/ZZZZZ"
/>
</ControlGroup>
</FormWrapper>
<Heading level={3}>Message Appearance</Heading>
<Paragraph>
The following settings will influence how messages will show up in Slack.{' '}
<Link onClick={() => openSlackMessagePreview(data)} openInNewContext>
Show Preview
</Link>
.
</Paragraph>
<FormWrapper>
<ControlGroup
label="Sender Name"
help="This name will appear in slack as the user sending the message."
>
<Text value={fromUserName} onChange={updateUser} disabled={loading} />
</ControlGroup>
<ControlGroup
label="Sender Icon"
help="The avatar/icon shown by the sender of the slack message. This URL needs to be accessible from the internet."
>
<Text value={fromUserIcon} onChange={updateUserIcon} disabled={loading} />
</ControlGroup>
</FormWrapper>
<ButtonGroup>
<div>
<Button label="Cancel" appearance="secondary" onClick={redirectToAlertListingPage} />
</div>
<div>
<Button label="Save" appearance="primary" disabled={loading || !isDirty} onClick={save} />
</div>
</ButtonGroup>
</>
);
}
24 changes: 24 additions & 0 deletions src/ui/pages/slack_alerts_setup/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import layout from '@splunk/react-page';
import Heading from '@splunk/react-ui/Heading';
import React from 'react';
import { SetupForm } from './form';
import { BodyWrapper, Dialog, DialogWrapper, TitleWrapper } from './styles';
import ToastMessages from '@splunk/react-toast-notifications/ToastMessages';

function SlackSetupPage() {
return (
<BodyWrapper>
<TitleWrapper>
<Heading level={1}>Slack Alerts Setup</Heading>
</TitleWrapper>
<DialogWrapper>
<Dialog>
<SetupForm />
</Dialog>
</DialogWrapper>
<ToastMessages />
</BodyWrapper>
);
}

layout(<SlackSetupPage />, { pageTitle: 'Slack Alert setup', hideAppBar: true });
14 changes: 14 additions & 0 deletions src/ui/pages/slack_alerts_setup/preview.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export function generateMessageJSON({ from_user, from_user_icon, message = 'Lorem ipsum dolor sit amet.' }) {
return {
text: message,
username: from_user,
icon_url: from_user_icon,
};
}

export function openSlackMessagePreview(data) {
const url = `https://api.slack.com/docs/messages/builder?msg=${encodeURIComponent(
JSON.stringify(generateMessageJSON(data))
)}`;
window.open(url);
}
Loading

0 comments on commit 0759d1d

Please sign in to comment.