Dashboards & Visualizations

How to use Splunk UI/dashboard in external app

snigam78
Explorer

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.

 

Labels (3)
0 Karma

ryanoconnor
Builder

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;

 

 

 

 

 

 

 

 

 

pgoldweic
Communicator

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).

0 Karma

ryanoconnor
Builder

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. 

 

 

pgoldweic
Communicator

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. 

 

0 Karma

ryanoconnor
Builder

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. 

pgoldweic
Communicator

Thanks so much @ryanoconnor for sharing all that information! Very useful indeed. 

0 Karma

richgalloway
SplunkTrust
SplunkTrust

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.

---
If this reply helps you, an upvote would be appreciated.