Examples
The following are some examples of how to use the Siren API to achieve different results.
Creating a "Hello World" script
You can create a very simple script as follows:
console.log("Hello World")
Accessing the current dashboard
You can get the object representing the current dashboard by using the following script:
sirenapi.Dashboard.getCurrentDashboard().then(dash => {
// your code here
});
Registering an event handler
You can register an event handler when the count changes on a dashboard’s entity table by using the following script:
sirenapi.Dashboard.getCurrentDashboard().then(dash => {
dash.on(sirenapi.Dashboard.EVENTS.SEARCH_COUNT_CHANGED, event => {
// your code here
// the event will give more information about which count was changed
// as there could be multiple counts on a single dashboard (dashboard 360)
});
});
Finding a visualization
You can find a visualization on the current dashboard by its title by using the following script:
const title = 'My visualization';
let vis;
const visualizations = dash.getVisualizationByTitle(title);
if (visualizations.length === 1) {
vis = visualizations[0];
} else if (visualizations.length === 0) {
throw new Error('Visualization "' + title + '" not found');
} else {
throw new Error('More than one visualization with the title "' + title + '" found');
}
Finding a dashboard’s entity table
You can find a dashboard’s entity table by its title by using the following script:
const title = 'My search';
let mySearch;
const searches = dash.getDashboardDataModelSearchByTitle(title);
if (searches.length === 1) {
mySearch = searches[0];
} else if (searches.length === 0) {
throw new Error('Search "' + title + '" not found');
} else {
throw new Error(
'More than one node in dashboard datamodel with search "' + title + '"'. +
'Use getDashboardDataModelSearchByNodeId instead'
);
}
Using special variables
When the script is attached to a visualization, you can use a special variable currentVisualization to access the Visualization object representing visualization to which the script was attached.
You can also use the special variable currentDashboard to access the Dashboard object representing the current dashboard on which the script is executed.
const dash = currentDashboard;
const vis = currentVisualization;
Using methods registered by visualizations
Any custom method that is registered by a visualization is exposed on the Visualization object.
For example, a visualization called MapX registered a custom method called reloadMap(). The script is attached to an instance of the MapX visualization. The method can be accessed by using the following script:
...
const myMapX = currentVisualization;
myMapX.reloadMap();
...
Adding a filter to current filters
You can add a filter to current filters by using the following script:
...
const myFilter = {
meta: {
alias: "My filter"
},
match_all: {}
};
dash.addFilters(myFilter);
dash.applyState();
...
You can add a filter as a pinned filter by setting the value of the meta.pinned
property to true
:
...
const myFilter = {
meta: {
alias: "My filter",
pinned: true
},
match_all: {}
};
dash.addFilters(myFilter);
dash.applyState();
...
Replacing current filters
You can replace current filters by using the following script:
...
const filters = dash.filters;
...
// manipulate filters
...
dash.setFilters(filters);
dash.applyState();
...
You can replace current pinned filters by setting the value of the meta.pinned
property to true
:
...
const myFilter = {
meta: {
alias: "My filter",
pinned: true
},
match_all: {}
};
dash.setFilters(filters);
dash.applyState();
...
Getting the current time of the dashboard
You can get the current time of the dashboard and create a time filter by using the following script:
...
const title = "my search";
const searches = dash.getDashboardDataModelSearchByTitle(title);
let mySearch;
if (searches.length === 1) {
mySearch = searches[0];
} else if (searches.length === 0) {
throw new Error('Search "' + title + '" not found');
} else {
throw new Error(
'More than one node in dashboard datamodel with search "' + title + '"'. +
'Use getDashboardDataModelSearchByNodeId instead'
);
}
const time = dash.getTime();
sirenapi.getTimeFilter(time, mySearch.id)
.then(timeFilter => {
// here we can use the timefilter created on correct field
})
Get any dashboard by id
You can get any dashboard by id by using the following script:
...
const dashboardId = 'dashboard:1';
const dashboardIdFilters = [
{
query: {
term: {
fieldA: 'valueA'
}
},
meta: {
alias: 'My filter'
}
}
]
sirenapi.Dashboard.fetchDashboardById(dashboardId)
.then(dash => {
dash.setFilters(dashboardIdFilters);
return dash.redirect();
})
.then(() => {
console.log('After redirect');
})
Opening a dashboard
You can get any dashboard, manipulate its state and open it by using the following script:
...
async function redirectToDashboard() {
const dashboardId = 'dashboard:1';
const dash = await sirenapi.Dashboard.fetchDashboardById(dashboardId);
dash.setQueryString('my new query')
dash.setFilters([
{
query: {
term: {
fieldA: 'valueA'
}
},
meta: {
alias: 'My new filter'
}
}
])
await dash.open();
}
...
Web services integration
You can integrate Web services by using the following script:
...
sirenapi.invokeWebService(groupName, serviceName, params, options)
.then(data => {
});
...
Adding records to collection
You can add the current record
, which is taken from the render function’s
PerspectiveInput argument,
to an existing collection by appending it to an array and passing the array as follows:
...
async function addRecordsToCollection() {
await sirenapi.Collection.addCollectionRecords({
recordIds: [record],
collectionId: 'graph:fe6a22b0-9b33-11ee-ba46-3bc5dbd48d95',
openAfterAdd: true // opens graph in UI
});
}
...
Not specifying a collectionId
on the Collection.addCollectionRecords
method will open a modal
which allows the user to either create a new collection or pick an existing one.
You can also pass other records to add to a collection by constructing them beforehand.
In addition to |
...
const constructedRecords = [
{
_id: '56X79I0BHl4rtxiYXMH8',
_index: 'article',
searchId: 'search:9b412ba0-533c-411e-b431-532db5d05260'
},
{
_id: 'yLAJ9Y0BHl4rtxiY_BV5',
_index: 'company',
searchId: 'search:5aae8ddd-d77e-476e-98ec-60bfd58dee9e'
}
];
async function addRecordsToCollection() {
await sirenapi.Collection.addCollectionRecords({
recordIds: constructedRecords,
allowCreate: true // displays additional "Add and open" button in modal
})
}
...
Here is an example of the modal:
Merging records from multiple graphs into a single graph
You can use the Graph
class to build a single merged graph from multiple graphs. Start from the Graph.getGraphsWithRecords
method to get all graphs containing given record(s):
...
async function enrichRecordData({ record, computedData }) {
computedData.graphs = await Graph.getGraphsWithRecords({ recordIds: [record] });
}
...
You can then use the getEsRecords
and getEidRecords
methods of the graph instances to get their records:
...
const records = _.flatten(await Promise.all(graphs.map(async graph => {
return _.flatten(await Promise.all([graph.getEsRecords(), graph.getEidRecords()]));
})));
...
Finally, you can pass the records into the Graph.addRecordsWithModal
function to create the merged graph:
...
await Graph.addRecordsWithModal({ recordIds: records });
...
The following script provides a complete example for selecting and merging graphs with a suitable user interface:
const { Graph, Collection } = sirenapi;
const {
EuiFlexGroup,
EuiFlexItem,
EuiTitle,
EuiTableBody,
EuiTable,
EuiTableRow,
EuiTableHeader,
EuiTableHeaderCell,
EuiTableRowCell,
EuiCheckbox,
EuiButton
} = Eui;
function getInitialData() {
return { graphsContainingRecord: [], recordLabel: '' }
}
async function enrichRecordData({ record, computedData }) {
computedData.recordLabel = await record.getLabel();
computedData.graphsContainingRecord = await Graph.getGraphsWithRecords({ recordIds: [record] });
}
function GraphsContainingRecord({ graphs, recordLabel }) {
const [selectedGraphsById, setSelectedGraphsById] = React.useState({});
const selectedGraphsCount = Object.keys(selectedGraphsById).length;
const getGraphRecords = async graph => {
return _.flatten(await Promise.all([graph.getEsRecords(), graph.getEidRecords()]));
}
const onMerge = async () => {
const graphs = Object.values(selectedGraphsById);
const records = _.flatten(await Promise.all(graphs.map(getGraphRecords)));
await Graph.addRecordsWithModal({ recordIds: records, openAfterAdd: true });
};
const onCheck = (graph) => {
const graphId = graph.getId();
const newSelectedGraphsById = {...selectedGraphsById};
if (newSelectedGraphsById[graphId]) {
delete newSelectedGraphsById[graphId];
} else {
newSelectedGraphsById[graphId] = graph;
}
setSelectedGraphsById(newSelectedGraphsById);
};
return (
<div>
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center" style={{ marginBottom: 20 }}>
<EuiFlexItem grow={false}>
<EuiTitle size="m">
<h2 style={{ margin: 0 }}>Graphs Containing {recordLabel}</h2>
</EuiTitle>
</EuiFlexItem>
</EuiFlexGroup>
<EuiFlexGroup justifyContent="center" alignItems="center">
<EuiFlexItem>
<EuiTable>
<EuiTableHeader>
<EuiTableHeaderCell style={{ width: 50, textAlign: 'center' }}>
<EuiCheckbox
id="selectAll"
checked={selectedGraphsCount > 0 && selectedGraphsCount === graphs.length}
onChange={(e) => {
let newSelectedGraphsById = {};
if (e.target.checked) {
graphs.forEach(graph => {
newSelectedGraphsById[graph.getId()] = graph;
});
}
setSelectedGraphsById(newSelectedGraphsById);
}}
/>
</EuiTableHeaderCell>
<EuiTableHeaderCell>Label</EuiTableHeaderCell>
<EuiTableHeaderCell>Count</EuiTableHeaderCell>
<EuiTableHeaderCell>Last Saved</EuiTableHeaderCell>
<EuiTableHeaderCell>Actions</EuiTableHeaderCell>
</EuiTableHeader>
<EuiTableBody>
{graphs.map((graph) => (
<EuiTableRow key={graph.getId()}>
<EuiTableRowCell style={{ width: 50, textAlign: 'center' }}>
<EuiCheckbox
id={`checkbox-${graph.getId()}`}
checked={selectedGraphsById[graph.getId()]}
onChange={() => onCheck(graph)}
/>
</EuiTableRowCell>
<EuiTableRowCell>{graph.getLabel()}</EuiTableRowCell>
<EuiTableRowCell>{graph.getCount()}</EuiTableRowCell>
<EuiTableRowCell>{graph.getLastSaveTime().toLocaleString()}</EuiTableRowCell>
<EuiTableRowCell>
<EuiButton size="s" onClick={() => graph.open({ newTab: true })}>
Open
</EuiButton>
</EuiTableRowCell>
</EuiTableRow>
))}
</EuiTableBody>
</EuiTable>
</EuiFlexItem>
</EuiFlexGroup>
<div style={{ textAlign: 'center', marginTop: 20 }}>
<EuiButton fill onClick={onMerge}>
Merge Graphs ({selectedGraphsCount})
</EuiButton>
</div>
</div>
);
}
function reactView({ computedData }) {
const { graphsContainingRecord, recordLabel } = computedData;
return (
<GraphsContainingRecord
graphs={graphsContainingRecord}
recordLabel={recordLabel}
/>
);
}
context.registerPerspectives({
view: {
'merge graphs': {
initialData: getInitialData,
enrich: enrichRecordData,
render: reactView
}
}
});
Opening records in the search modal
You can initialize a simplified search app session by calling the SearchApp.openSearchModal
method.
The method accepts an optional options parameter; not providing this argument will initialize a new search session.
The available options can be read about in the core api
docs here.
In the following example, we pass the recordIds
which contains the record for Google in the company index into the modal.
As well as a title to identify the modal instance:
...
const recordIds = [{
_id: 'PAH68YsBTYjjQzpeJyru',
_index: 'company',
searchId: 'search:5aae8ddd-d77e-476e-98ec-60bfd58dee9e'
}];
async function openRecordInSearchAppModal() {
await sirenapi.SearchApp.openSearchModal({
recordIds,
modalTitle: 'Search record for Google',
confirmButtonLabel: 'Get selected records'
})
}
...
This is an example of the resulting modal:
Building simple forms
The scripted panel API allows for a rapid creation of simple HTML forms.
The script below is an example of a form that takes two inputs and executes a function when the user presses a Submit button.
...
function onSubmit(formData) {
console.log(formData);
}
const info = currentVisualization.getHtmlElement(
'<div>' +
'<p>Enter data and press the button.</p>' +
'</div>'
);
const input1 = currentVisualization.getHtmlTextInputElement('name', 'Name', 'Enter your name', "");
const input2 = currentVisualization.getHtmlTextInputElement('itemsNo', 'Items Number', 'Enter number of items', "3");
const submit = currentVisualization.getHtmlSubmitButtonElement('Find');
const form = currentVisualization.getHtmlFormElement(onSubmit);
form.appendChild(info);
form.appendChild(input1);
form.appendChild(input2);
form.appendChild(submit);
currentVisualization.appendHtmlElement(form);
Building an interactive table
One of the components provided by the scripted panel API is an interactive table.
The script below shows how this table is created, with a callback that handles rows being selected.
...
// Adds interactive table to panel
const columns = [
{ name: 'Column 1', field: 'field1' },
{ name: 'Column 2', field: 'field2' }
];
const data = [
{ field1: 'value1', field2: 'value2' },
{ field1: 'value3', field2: 'value4' }
];
table = currentVisualization.getInteractiveTable({ columns, data, onSelect });
currentVisualization.appendHtmlElement(table);
// Updates dashboard filter when rows are selected
function onSelect(selectedItems) {
currentDashboard.setFilters([{
meta: {
alias: 'Selected items'
},
query: {
bool: {
minimum_should_match: 1,
should: selectedItems.map(item => ({
match_phrase: {
'label': item.field1
}
}))
}
}
}]);
currentDashboard.applyState();
}
Developing plug-ins that are compatibile with sirenAPI
First, you must register an interface with definitions of all custom methods that are used by your plug-in. This will tell the sirenAPI
which versions of the API are supported by your plug-in.
import scriptingInterfaces from 'scripting-interfaces';
...
if ($injector.has('actionRegistry')) {
const actionRegistry = $injector.get('actionRegistry');
const apiVersion = '1';
actionRegistry.registerInterfaceForType(
apiVersion,
'my_plugin_id',
scriptingInterfaces.v1.visualizations.my_visualization
);
}
To enable a visualization to interact with SirenAPI, a plug-in developer must register the required actions by using SirenAPI.
To register methods, use the actionRegister service in your visualization controller.
...
if ($injector.has('actionRegistry')) {
const actionRegistry = $injector.get('actionRegistry');
const apiVersion = '1';
actionRegistry.register(apiVersion, $scope.vis.id, 'myMethod', async (param1, param2) => {
// should ALWAYS return a promise !!!
});
}
...
The plug-in developer can also emit custom events that scripts can listen to.
To emit a custom event, use the following script:
...
if ($injector.has('sirenAPI')) {
const sirenAPI = $injector.get('sirenAPI');
const data = {
name: 'my-custom-event',
...
}
sirenapi.emit(sirenapi.EVENTS.CUSTOM, data);
};
...
In the script, you register event handlers and call methods as follows:
...
sirenapi.on(sirenapi.EVENTS.CUSTOM, event => {
if (event.data.name === 'my-custom-event') {
vis.myMethod();
}
});
...
sirenAPI.es
The Elasticsearch client. For more information, see the Elasticsearch API reference documentation.
Example:
async function sendSearchQuery() {
try {
const { hits } = await sirenapi.es.search({
index: 'company',
body: {
size: 1,
query: {
match: { label: 'google' }
}
}
})
console.log('Got hits', hits)
} catch (err) {
console.log('Got error:', hits)
}
}
sendSearchQuery();
sirenAPI.Charts
In this script, you create a chart from ApexCharts and attach it to a scripted panel visualization.
The ApexCharts library provides many types of interactive charts. For more information, go to the ApexCharts website.
Example:
const options = {
series: [44, 55, 41, 17, 15],
labels: ['Comedy', 'Action', 'SciFi', 'Drama', 'Horror'],
chart: {
type: 'donut',
width: 360
}
};
sirenapi.Charts.createApexChart(options).then(chart => {
currentVisualization.appendHtmlElement(chart.getHtmlElement());
chart.render();
});
Adding components built on top of React and Elastic UI
To enable this feature make sure the following configuration is enabled in investigate.yml
|
siren_scripting:
librariesWhitelist: ['React', 'EUI']
The scripted panel API allows adding custom components writting using React library. This script below shows how to add a simple button.
...
const buttonStyle = {
border: '1px solid red',
padding: '10px',
borderRadius: '2px'
}
// A react component
function SimpleButton() {
const onClick = () => {
console.log('button clicked');
}
return <div>
<button style={buttonStyle} onClick={onClick}>Simple Button</button>
</div>
}
currentVisualization.renderReactElement({
Element: <SimpleButton/>
});
Results of the above script
React components from Elastic UI can also be used to build the custom components.
...
const {
EuiButton
} = Eui;
// A react element wit Eui button
function SimpleEuiButton() {
const onClick = () => {
console.log('button clicked');
}
return <div>
<EuiButton onClick={onClick}>Simple Button</EuiButton>
</div>
}
currentVisualization.renderReactElement({
Element: <SimpleEuiButton/>
});
Results of the above script
A more complex example to display the counts of the dashboard on a scripted panel.
const {
EuiStat
} = Eui;
// This React component listens to SEARCH_COUNT_CHANGED event and display the count
function DisplayCounts(props) {
const { dashboard } = props;
const [count, setCount] = React.useState(0);
React.useEffect(() => {
dashboard.on(sirenapi.Dashboard.EVENTS.SEARCH_COUNT_CHANGED, e => setCount(e.count))
}, [dashboard]);
return <div>
<EuiStat title={count} description={"Counts"} textAlign="right" />
</div>
}
currentVisualization.renderReactElement({
Element: <DisplayCounts
dashboard={currentDashboard}
/>
});
Results of the above script
React components can also be shown in a modal over a dashboard or a visualization.
...
function ModalContentElement() {
return <div>Modal Content</div>
}
// Open a modal over the visualization the script is attached to
currentVisualization.openModal(
{
Element: <ModalContentElement/>,
titleText: 'Modal title',
primaryText: 'Ok',
onPrimaryClick: () => {},
cancelText: 'Cancel',
onCancel: () => {},
});
// Open a modal over the dashboard the script is attached to
currentDashboard.openModal(
{
Element: <ModalContentElement/>,
titleText: 'Modal title',
onPrimaryClick: () => {},
cancelText: 'Cancel',
onCancel: () => {}
});