we are currently exploring splunkjs for rendering data in our custom app. we are able to authenticate and display charts based on searches directly from webapp but having difficulty in integrating with react app as its not component based.
we saw the new Splunk ui/dashboard studio with many react components e.g. splunk/react-ui ,splunk-visualizations
we think we can use this react components in our external webapp butwe are not able to see any authentication mechanism in these react components.
how can we use these react components in external app which goes against splunk enterprise does authentication fires searches and displays charts.
Thanks in advance.
This is a great question @snigam78 and something my team is currently interested in providing examples for. Would love to hear more about your use case if you have time.
We do have a couple of packages available on https://splunkui.splunk.com that I think are helpful. Here is some javascript that utilizes @splunk/splunk-utils and the @splunk/visualizations package to query Splunk from an external location, pull back some data, and populate a single value, and a column chart.
This example allows allows you to execute post-process searches, and dynamically add new visualizations to your page.
import './App.css';
import { useState, useEffect, useCallback } from 'react';
import { presets, formInputTypes } from './constants';
//@splunk/visualizations imports
//These are visualizations we are using for this demo
import SingleValue from '@splunk/visualizations/SingleValue';
import Column from '@splunk/visualizations/Column';
//@splunk/react-ui imports.
//These are what give us components that look and feel like Splunk.
import Link from '@splunk/react-ui/Link';
import List from '@splunk/react-ui/List';
import P from '@splunk/react-ui/Paragraph';
import Button from '@splunk/react-ui/Button';
import WaitSpinner from '@splunk/react-ui/WaitSpinner';
import Heading from '@splunk/react-ui/Heading';
import Switch from '@splunk/react-ui/Switch';
//@splunk/react-search imports.
//These are what give us a search bar and time picker
import SearchBar from '@splunk/react-search/components/Bar';
import Input from '@splunk/react-search/components/Input';
//@splunk/splunk-utils imports.
//This is what is used to create search jobs
import { createSearchJob, getData } from '@splunk/splunk-utils/search';
//Custom Components
import LoginComponent from './components/LoginComponent';
function App() {
//State variables for communication with Splunkd
const queryParams = new URLSearchParams(window.location.search);
const [sessionKey, setSessionKey] = useState('<Token>');
const [username, setUsername] = useState(queryParams.get('username'));
const [password, setPassword] = useState(queryParams.get('password'));
const [serverURL, setServerURL] = useState(queryParams.get('serverURL'));
const headers = {
headers: {
Authorization: `Splunk ${sessionKey}`,
},
};
/* Second Visualization Variables */
//Sid for Column Chart
const [columnSid, setColumnSid] = useState();
//Search for Column Chart
const [splunkSearchColumn, setSplunkSearchColumn] = useState(
'search index=_* | stats count by sourcetype | eval count=random()%200 | fields sourcetype count'
);
const [splunkSearchColumnEarliest, setSplunkSearchColumnEarliest] = useState('-24h');
const [splunkSearchColumnLatest, setSplunkSearchColumnLatest] = useState('now');
const [columnSearching, setColumnSearching] = useState(false);
//Fields for Column Chart
const [columnSearchResultsFields, setColumnSearchResultsFields] = useState();
//Columns for Column Chart
const [columnSearchResultsColumns, setColumnSearchResultsColumns] = useState();
//Seconds to Complete for Column Chart
const [columnSecondsToComplete, setColumnSecondsToComplete] = useState();
const [columnSearchOptions, setColumnSearchOptions] = useState({
earliest: splunkSearchColumnEarliest,
latest: splunkSearchColumnLatest,
search: splunkSearchColumn,
timePickerPresets: presets,
timePickerFormInputTypes: formInputTypes,
timePickerAdvancedInputTypes: [],
});
const [columnSearchObj, setColumnSearchObj] = useState({
search: '',
earliest: '',
latest: '',
});
const [columnAppendPostProcess, setColumnAppendPostProcess] = useState(false);
const [columnViz, setColumnViz] = useState([]);
/* Second Visualization Post Process Variables */
const [splunkSearchColumnPostProcess, setSplunkSearchColumnPostProcess] = useState(
'| search sourcetype="splunk*" OR sourcetype="*scheduler*" | sort 0 - count'
);
const handleColumnAppendPostProcessClick = useCallback(() => {
setColumnAppendPostProcess((current) => !current);
}, []);
const columnPostProcessBar = (
<>
<div>
<div style={{ float: 'left' }}>
<Switch
value={false}
onClick={handleColumnAppendPostProcessClick}
selected={columnAppendPostProcess}
appearance="toggle"
error={!columnAppendPostProcess}
></Switch>
</div>
<div>
<Heading level={4} style={{ paddingLeft: '40px', paddingTop: '10px' }}>
{columnAppendPostProcess
? ' Append Visualization'
: ' Update Existing'}
</Heading>
</div>
</div>
<Input
value={splunkSearchColumnPostProcess}
onChange={(e, value) =>
handlePostProcessChange(e, value, setSplunkSearchColumnPostProcess)
}
onEnter={() =>
handleEventTrigger(
columnSid,
splunkSearchColumnPostProcess,
setColumnSearchResultsFields,
setColumnSearchResultsColumns
)
}
/>
</>
);
//Sid for Single Value
const [singleValueSid, setSingleValueSid] = useState();
//Search for Single Value
const [splunkSearchSingleValue, setSplunkSearchSingleValue] = useState(
'search index=_internal | stats count by sourcetype'
);
const [splunkSearchSingleValueEarliest, setSplunkSearchSingleValueEarliest] = useState('-24h');
const [splunkSearchSingleValueLatest, setSplunkSearchSingleValueLatest] = useState('now');
const [singleValueSearching, setSingleValueSearching] = useState(false);
//Fields for Single Value
const [singleValueSearchResultsFields, setSingleValueSearchResultsFields] = useState();
//Columns for Single Value
const [singleValueSearchResultsColumns, setSingleValueSearchResultsColumns] = useState();
//Seconds to Complete for Single Value
const [singleValueSeondsToComplete, setSingleValueSecondsToComplete] = useState();
const [singleValueSearchOptions, setSingleValueSearchOptions] = useState({
earliest: splunkSearchSingleValueEarliest,
latest: splunkSearchSingleValueLatest,
search: splunkSearchSingleValue,
timePickerPresets: presets,
timePickerFormInputTypes: formInputTypes,
timePickerAdvancedInputTypes: [],
});
const [singleValueSearchObj, setSingleValueSearchObj] = useState({
search: '',
earliest: '',
latest: '',
});
const [singleValueAppendPostProcess, setSingleValueAppendPostProcess] = useState(false);
const [singleValueViz, setSingleValueViz] = useState([]);
const [splunkSearchSingleValuePostProcess, setSplunkSearchSingleValuePostProcess] = useState(
'| search sourcetype="splunkd"'
);
const handleSingleValueAppendPostProcessClick = () => {
setSingleValueAppendPostProcess(!singleValueAppendPostProcess);
};
const singleValuePostProcessBar = (
<>
<div>
<div style={{ float: 'left' }}>
<Switch
onClick={handleSingleValueAppendPostProcessClick}
selected={singleValueAppendPostProcess}
appearance="toggle"
error={!singleValueAppendPostProcess}
selectedLabel="Append Visualization"
unselectedLabel="Update Existing Visualization"
></Switch>
</div>
<div>
<Heading level={4} style={{ paddingLeft: '40px', paddingTop: '10px' }}>
{singleValueAppendPostProcess
? ' Append Visualization'
: ' Update Existing'}
</Heading>
</div>
</div>
<Input
value={splunkSearchSingleValuePostProcess}
onChange={(e, value) =>
handlePostProcessChange(e, value, setSplunkSearchSingleValuePostProcess)
}
onEnter={() =>
handleEventTrigger(
singleValueSid,
splunkSearchSingleValuePostProcess,
setSingleValueSearchResultsFields,
setSingleValueSearchResultsColumns
)
}
/>
</>
);
//Timer for Search length
const timer = (ms) => new Promise((res) => setTimeout(res, ms));
async function load(sidJob, completeFunc, fieldsFunc, columnsFunc, setSearchingBool, type) {
var completeSeconds = 0;
for (var i = 0; i < 30; i++) {
fetchData(sidJob, fieldsFunc, columnsFunc, type)
.then((data) => data)
.then((sidJob) => {
if (sidJob) {
completeSeconds = completeSeconds + 1;
setSearchingBool(false);
completeFunc(completeSeconds);
}
});
if (!completeSeconds) {
await timer(1000);
} else {
break;
}
}
}
//Function for Clicking the Post Process Search Button
async function handlePostProcessClick(
locaPostProcessSid,
postProcessSearch,
setFields,
setColumns,
appendBool,
type
) {
postProcess(locaPostProcessSid, postProcessSearch, setFields, setColumns, appendBool, type);
}
//Function for Updating the Post Process Search
function handlePostProcessChange(e, value, setPostProcess) {
setPostProcess(value.value);
}
const createJob = async (search, earliest, latest) => {
const n = createSearchJob(
{
search: search,
earliest_time: earliest,
latest_time: latest,
},
{},
{ splunkdPath: serverURL, app: 'search', owner: username },
headers
)
.then((response) => response)
.then((data) => data.sid);
return n;
};
const fetchData = async (sidJob, fieldsFunc, columnsFunc, type) => {
const n = await getData(
sidJob,
'results',
{ output_mode: 'json_cols' },
{ splunkdPath: serverURL, app: 'search', owner: username },
headers
)
.then((response) => response)
.then((data) => {
if (data) {
fieldsFunc(data.fields);
columnsFunc(data.columns);
if (type == 'SingleValue') {
setSingleValueViz([
<SingleValue
options={{
majorColor: '#008000',
sparklineDisplay: 'off',
trendDisplay: 'off',
}}
dataSources={{
primary: {
data: {
fields: data.fields,
columns: data.columns,
},
meta: {},
},
}}
/>,
]);
}
if (type == 'Column') {
setColumnViz([
<Column
options={{}}
dataSources={{
primary: {
data: {
fields: data.fields,
columns: data.columns,
},
meta: {},
},
}}
/>,
]);
}
return data;
}
});
return n;
};
const postProcess = async (sidJob, postProcess, fieldsFunc, columnsFunc, appendBool, type) => {
const n = await getData(
sidJob,
'results',
{ output_mode: 'json_cols', search: postProcess },
{ splunkdPath: serverURL, app: 'search', owner: username },
headers
)
.then((response) => response)
.then((data) => {
if (data) {
fieldsFunc(data.fields);
columnsFunc(data.columns);
if (appendBool) {
if (type == 'SingleValue') {
setSingleValueViz([
...singleValueViz,
<SingleValue
options={{
majorColor: '#008000',
sparklineDisplay: 'off',
trendDisplay: 'off',
}}
dataSources={{
primary: {
data: {
columns: data.columns,
fields: data.fields,
},
meta: {},
},
}}
/>,
]);
}
if (type == 'Column') {
setColumnViz([
...columnViz,
<Column
options={{}}
dataSources={{
primary: {
data: {
columns: data.columns,
fields: data.fields,
},
meta: {},
},
}}
/>,
]);
}
} else {
if (type == 'SingleValue') {
setSingleValueViz([
<SingleValue
options={{
majorColor: '#008000',
sparklineDisplay: 'off',
trendDisplay: 'off',
}}
dataSources={{
primary: {
data: {
columns: data.columns,
fields: data.fields,
},
meta: {},
},
}}
/>,
]);
}
if (type == 'Column') {
setColumnViz([
<Column
options={{}}
dataSources={{
primary: {
data: {
columns: data.columns,
fields: data.fields,
},
meta: {},
},
}}
/>,
]);
}
}
return data;
}
});
return n;
};
const handleOptionsChange = async (option, setSearchOptions, searchOptions) => {
setSearchOptions({
...searchOptions,
...option,
});
};
/**
* Invoked when the user hits enter or click on the search button
*/
const handleEventTrigger = async (
eventType,
Sid,
setSidFunc,
setSearchObjFunction,
searchObj,
setSecondsToComplete,
setSearchResultsFields,
setSearchResultsColumns,
setSearchingBool,
setOptionsFunc,
searchOptions,
type
) => {
setSearchObjFunction({
search: searchOptions.search,
earliest: searchOptions.earliest,
latest: searchOptions.latest,
});
switch (eventType) {
case 'submit':
setSearchingBool(true);
createJob(searchOptions.search, searchOptions.earliest, searchOptions.latest)
.then((data) => data)
.then((sidJob) => {
setSidFunc(sidJob);
load(
sidJob,
setSecondsToComplete,
setSearchResultsFields,
setSearchResultsColumns,
setSearchingBool,
type
);
});
break;
case 'escape':
this.handleOptionsChange({ search: '' }, setOptionsFunc, searchOptions);
break;
default:
break;
}
};
const wordBreakStyle = { overflowWrap: 'break-word', margin: '10px' };
return (
<div className="App">
<header className="App-header">
<Heading level={1}>@splunk/splunk-utils Example app</Heading>
<P>
This app will show you how to query Splunk from a remote webapp using our Splunk
UI Toolkit in React. It uses a couple of packages listed below:{' '}
</P>
<List>
<List.Item>
<Link to="https://www.npmjs.com/package/@splunk/splunk-utils">
@splunk/splunk-utils
</Link>
</List.Item>
<ul>
<li>
<Link to="https://splunkui.splunkeng.com/Packages/splunk-utils">
Documentation
</Link>
</li>
</ul>
<List.Item>
<Link to="https://www.npmjs.com/package/@splunk/visualizations">
@splunk/visualizations
</Link>
</List.Item>
<ul>
<li>
<Link to="https://splunkui.splunkeng.com/Packages/visualizations">
Documentation
</Link>
</li>
</ul>
<List.Item>
<Link to="https://www.npmjs.com/package/@splunk/react-ui">
@splunk/react-ui
</Link>
</List.Item>
<ul>
<li>
<Link to="https://splunkui.splunkeng.com/Packages/react-ui">
Documentation
</Link>
</li>
</ul>
</List>
{sessionKey == '<Token>' ? (
<>
<Heading level={2}>Setup Instructions</Heading>
<P>
Note: You may need to complete a step for this app to work with your
Splunk Environment. Details below:
</P>
<List>
<List.Item>
You'll need to configure CORS on your Splunk Environment.
Instructions can be found{' '}
<Link to="https://dev.splunk.com/enterprise/docs/developapps/visualizedata/usesplunkjsstack/communicatesplunkserver/">
here
</Link>
</List.Item>
<List.Item>
You'll need to have a trusted certificate for the Splunk management
port. If you don't have a valid certificate, you can always visit
the URL for the management port of your Splunk environment, and
trust the certificate manually with your browser.
</List.Item>
</List>
</>
) : (
<></>
)}
{sessionKey == '<Token>' ? (
<>
<LoginComponent
username={username}
setUsername={setUsername}
password={password}
setPassword={setPassword}
serverURL={serverURL}
setServerURL={setServerURL}
sessionKey={sessionKey}
setSessionKey={setSessionKey}
></LoginComponent>
</>
) : (
<div style={{ width: '100%' }}>
<div style={{ float: 'left', width: '47%', padding: '10px' }}>
<Heading style={wordBreakStyle} level={3}>
This is a Single Value that is populated by the following search:{' '}
</Heading>
<div style={{ padding: '10px' }}>
<SearchBar
options={singleValueSearchOptions}
onOptionsChange={(options) =>
handleOptionsChange(
options,
setSingleValueSearchOptions,
singleValueSearchOptions
)
}
onEventTrigger={(eventType) =>
handleEventTrigger(
eventType,
singleValueSid,
setSingleValueSid,
setSingleValueSearchObj,
singleValueSearchObj,
setSingleValueSecondsToComplete,
setSingleValueSearchResultsFields,
setSingleValueSearchResultsColumns,
setSingleValueSearching,
setSingleValueSearchOptions,
singleValueSearchOptions,
'SingleValue'
)
}
/>
</div>
{singleValueSearching ? <WaitSpinner size="medium" /> : <></>}
{singleValueSeondsToComplete ? (
<>
{singleValueViz.map((key, value) => {
return key;
})}
<Heading style={wordBreakStyle} level={3}>
Clicking this button will execute the following post-process
search:{' '}
</Heading>
{singleValuePostProcessBar}
<Button
label="Execute Post-process"
appearance="primary"
onClick={() =>
handlePostProcessClick(
singleValueSid,
splunkSearchSingleValuePostProcess,
setSingleValueSearchResultsFields,
setSingleValueSearchResultsColumns,
singleValueAppendPostProcess,
'SingleValue'
)
}
/>
<P style={wordBreakStyle}>
Search: {singleValueSearchOptions.search}
</P>
<P style={wordBreakStyle}>{'Splunk SID: ' + singleValueSid}</P>
<P style={wordBreakStyle}>
{'Seconds to Complete: ' +
JSON.stringify(singleValueSeondsToComplete)}
</P>
<P style={wordBreakStyle}>
{'Splunk Results - Fields: ' +
JSON.stringify(singleValueSearchResultsFields)}
</P>
<P style={wordBreakStyle}>
{'Splunk Results - Columns: ' +
JSON.stringify(singleValueSearchResultsColumns)}
</P>
</>
) : (
<></>
)}
</div>
<div style={{ float: 'right', width: '47%', padding: '10px' }}>
<Heading style={wordBreakStyle} level={3}>
This is a Column Chart that is populated by the following search:{' '}
</Heading>
<div style={{ padding: '10px' }}>
<SearchBar
options={columnSearchOptions}
onOptionsChange={(options) =>
handleOptionsChange(
options,
setColumnSearchOptions,
columnSearchOptions
)
}
onEventTrigger={(eventType) =>
handleEventTrigger(
eventType,
columnSid,
setColumnSid,
setColumnSearchObj,
columnSearchObj,
setColumnSecondsToComplete,
setColumnSearchResultsFields,
setColumnSearchResultsColumns,
setColumnSearching,
setColumnSearchOptions,
columnSearchOptions,
'Column'
)
}
/>
</div>
{columnSearching ? <WaitSpinner size="medium" /> : <></>}
{columnSecondsToComplete ? (
<>
{columnViz.map((key, value) => {
return key;
})}
<Heading style={wordBreakStyle} level={3}>
Clicking this button will execute the following post-process
search:{' '}
</Heading>
{columnPostProcessBar}
<Button
label="Execute Post-process"
appearance="primary"
onClick={() =>
handlePostProcessClick(
columnSid,
splunkSearchColumnPostProcess,
setColumnSearchResultsFields,
setColumnSearchResultsColumns,
columnAppendPostProcess,
'Column'
)
}
/>
<P style={wordBreakStyle}>
Search: {columnSearchOptions.search}
</P>
<P>{'Splunk SID: ' + columnSid}</P>
<P style={wordBreakStyle}>
{'Seconds to Complete: ' +
JSON.stringify(columnSecondsToComplete)}
</P>
<P style={wordBreakStyle}>
{'Splunk Results - Fields: ' +
JSON.stringify(columnSearchResultsFields)}
</P>
<P style={wordBreakStyle}>
{'Splunk Results - Columns: ' +
JSON.stringify(columnSearchResultsColumns)}
</P>
</>
) : (
<></>
)}
</div>
</div>
)}
</header>
</div>
);
}
export default App;
While I didn't write the original post, I hope it's okay to reply to @ryanoconnor here with a couple of follow-up questions on the code he posted. First of all, thanks so much for posting this code! Here are the questions:
1- Does the authentication (via the Authorization header) require *only* the new authentication tokens, or does it also work with session key values (retrieved via the legacy login REST endpoint)?
2- When writing these apps outside of Splunk, can we make use of Splunk css files similarly to what we could do with splunkjs? (in splunkjs-based apps, we're able to keep all the relevant splunk-related css files in a special static/splunkjs app folder). These css files could then be used within the React code (similarly to how you use App.css in the example).
Hello! Happy to report back some news here.
1. This will work with a sessionKey that can be obtained from a REST call to the /services/auth/login endpoint documented here (I've updated the code above).
2. I believe the answer is yes here. But I might need to dig in more to your use case to see what you have. I would also recommend looking at our Splunk UI components and also our @splunk/themes package, as that may solve this more easily for you: https://splunkui.splunk.com/Packages/themes/Overview
Hope this helps!
EDIT: I am keeping the code in the comment above. As pasting multiple versions in here is getting unwieldy.
Thanks so much for your updated post @ryanoconnor . Would be great if you could also share two files that your code requires: ."/App.css", and also "./constants", which should help with running this sample in a real environment. Or, at least share the "constants" file only, since I now realize that "App.css" might be the one created automatically by create-react-app.
No problem! And apologies for the delay on this. I do plan to hopefully open source this code which will make life easier in the future for this example.
App.css
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
constants.js
export const formInputTypes = ['relative'];
export const presets = [
{ label: 'Today', earliest: '@d', latest: 'now' },
{ label: 'Week to date', earliest: '@w0', latest: 'now' },
{ label: 'Business week to date', earliest: '@w1', latest: 'now' },
{ label: 'Month to date', earliest: '@mon', latest: 'now' },
{ label: 'Year to date', earliest: '@y', latest: 'now' },
{ label: 'Yesterday', earliest: '-1d@d', latest: '@d' },
{ label: 'Previous week', earliest: '-7d@w0', latest: '@w0' },
{ label: 'Previous business week', earliest: '-6d@w1', latest: '-1d@w6' },
{ label: 'Previous month', earliest: '-1mon@mon', latest: '@mon' },
{ label: 'Previous year', earliest: '-1y@y', latest: '@y' },
{ label: 'Last 15 minutes', earliest: '-15m', latest: 'now' },
{ label: 'Last 60 minutes', earliest: '-60m@m', latest: 'now' },
{ label: 'Last 4 hours', earliest: '-4h@m', latest: 'now' },
{ label: 'Last 24 hours', earliest: '-24h@h', latest: 'now' },
{ label: 'Last 7 days', earliest: '-7d@h', latest: 'now' },
{ label: 'Last 30 days', earliest: '-30d@d', latest: 'now' },
{ label: 'All time', earliest: '0', latest: '' },
];
FYI The constants file should have been in the docs here https://splunkui.splunk.com/Packages/react-search/ but I realize it's not. I'll try to get that corrected.
Thanks so much @ryanoconnor for sharing all that information! Very useful indeed.
Splunk Dashboard Studio is a new product that is still evolving so I suspect your use case has not yet been implemented. Go to https://ideas.splunk.com to ask them to move it up the priority list.