Skip to content

Instantly share code, notes, and snippets.

@cutler-scott-newrelic
Created August 12, 2020 22:27
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cutler-scott-newrelic/987c2365ace54785112d91318aca064c to your computer and use it in GitHub Desktop.
Save cutler-scott-newrelic/987c2365ace54785112d91318aca064c to your computer and use it in GitHub Desktop.
Netflow Nerdlet index
import React from 'react'
import PropTypes from 'prop-types'
import {
Grid,
GridItem,
Stack,
StackItem,
TextField,
Button,
LineChart,
BarChart,
TableChart,
HeadingText,
Select,
SelectItem,
PlatformStateContext,
UserStorageMutation,
Card,
CardHeader,
CardBody
} from 'nr1';
export default class NetflowNerdletV4 extends React.Component {
static propTypes = {
entity: PropTypes.object,
entities: PropTypes.array,
entityCount: PropTypes.object,
entitiesById: PropTypes.object,
entitiesByDomainType: PropTypes.object,
relationshipsById: PropTypes.object,
summaryDataById: PropTypes.object,
isLoadingEntities: PropTypes.bool,
headerState: PropTypes.object,
nr1: PropTypes.object,
width: PropTypes.number,
height: PropTypes.number,
}
constructor(props) {
super(props);
this.accountId = 1;
this.state = {
entityGuid: null,
appName: null,
trafficDirectionFilter: "Combined",
ipProtocolNumberAllow: [],
ipAddressAllow: [],
ipProtocolNumberBlock: [],
ipAddressBlock: [],
transportPortAllow: [],
transportPortBlock: [],
customNrqlValue: "",
ipInfoJson: {}
};
console.debug("Nerdlet constructor", this); //eslint-disable-line
this.openEntity = this.openEntity.bind(this);
}
setApplication(inAppId, inAppName) {
this.setState({
entityGuid: inAppId,
appName: inAppName,
trafficDirectionFilter: this.state.trafficDirectionFilter,
ipProtocolNumberAllow: this.state.ipProtocolNumberAllow,
ipAddressAllow: this.state.ipAddressAllow,
ipProtocolNumberBlock: this.state.ipProtocolNumberBlock,
ipAddressBlock: this.state.ipAddressBlock,
transportPortAllow: this.state.transportPortAllow,
transportPortBlock: this.state.transportPortBlock,
customNrqlValue: this.state.customNrqlValue,
ipInfoJson: this.state.ipInfoJson
})
}
setFilters(trafficdir, protocolnumallow, ipaddrallow, protocolnumblock, ipaddrblock, transportportallow, transportportblock) {
// console.log("setFilters(%s, %o, %o, %o, %o, %o, %o)", trafficdir, protocolnumallow, ipaddrallow, protocolnumblock, ipaddrblock, transportportallow, transportportblock);
this.setState({
entityGuid: this.state.entityGuid,
appName: this.state.appName,
trafficDirectionFilter: trafficdir,
ipProtocolNumberAllow: protocolnumallow,
ipAddressAllow: ipaddrallow,
ipProtocolNumberBlock: protocolnumblock,
ipAddressBlock: ipaddrblock,
transportPortAllow: transportportallow,
transportPortBlock: transportportblock,
customNrqlValue: this.state.customNrqlValue,
ipInfoJson: this.state.ipInfoJson
})
}
setCustomNrql(customNrqlValue) {
this.setState({
entityGuid: this.state.entityGuid,
appName: this.state.appName,
trafficDirectionFilter: this.state.trafficDirectionFilter,
ipProtocolNumberAllow: this.state.ipProtocolNumberAllow,
ipAddressAllow: this.state.ipAddressAllow,
ipProtocolNumberBlock: this.state.ipProtocolNumberBlock,
ipAddressBlock: this.state.ipAddressBlock,
transportPortAllow: this.state.transportPortAllow,
transportPortBlock: this.state.transportPortBlock,
customNrqlValue: customNrqlValue,
ipInfoJson: this.state.ipInfoJson
})
}
setIPInfo(ipInfoJson) {
this.setState({
entityGuid: this.state.entityGuid,
appName: this.state.appName,
trafficDirectionFilter: this.state.trafficDirectionFilter,
ipProtocolNumberAllow: this.state.ipProtocolNumberAllow,
ipAddressAllow: this.state.ipAddressAllow,
ipProtocolNumberBlock: this.state.ipProtocolNumberBlock,
ipAddressBlock: this.state.ipAddressBlock,
transportPortAllow: this.state.transportPortAllow,
transportPortBlock: this.state.transportPortBlock,
customNrqlValue: this.state.customNrqlValue,
ipInfoJson: ipInfoJson
})
}
autoSetFilters() {
this.setFilters(
this.state.trafficDirectionFilter,
this.state.ipProtocolNumberAllow,
this.state.ipAddressAllow,
this.state.ipProtocolNumberBlock,
this.state.ipAddressBlock,
this.state.transportPortAllow,
this.state.transportPortBlock
);
}
removeItemOnce(arr, value) {
var index = arr.indexOf(value);
if (index > -1) {
arr.splice(index, 1);
}
return arr;
}
createAddButton(array, tempTextBoxValues, valueToPush) {
//console.log("Button Add Value: " + tempTexBoxValues[valueToPush]);
return <Button
onClick={ () =>
{
array.push(tempTextBoxValues[valueToPush]);
this.autoSetFilters();
}
}
type={Button.TYPE.PRIMARY}
iconType={Button.ICON_TYPE.INTERFACE__SIGN__PLUS__V_ALTERNATE}/>
}
createTextEntryField(labelText, nameOfClass, tempTextBoxKey, tempTextBoxValues) {
return <TextField
label={labelText}
placeholder={labelText}
onChange={(evt) => tempTextBoxValues[tempTextBoxKey] = evt.target.value}
className={nameOfClass}/>
}
populateTopToolbarStack(tempTextBoxValues) {
// console.log(tempTextBoxValues)
let objectsToRender = [];
objectsToRender.push(this.createTextEntryField("IP protocol allow", "ipProtocolAllowFilterClass", "tempIpProtocolAllowTextboxValue", tempTextBoxValues));
objectsToRender.push(this.createAddButton(this.state.ipProtocolNumberAllow, tempTextBoxValues, "tempIpProtocolAllowTextboxValue"));
objectsToRender.push(this.createTextEntryField("IP protocol block", "ipProtocolBlockFilterClass", "tempIpProtocolBlockTextboxValue", tempTextBoxValues));
objectsToRender.push(this.createAddButton(this.state.ipProtocolNumberBlock, tempTextBoxValues, "tempIpProtocolBlockTextboxValue"));
objectsToRender.push(this.createTextEntryField("IP address allow", "ipAddressAllowFilterClass", "tempIpAddressAllowTextboxValue", tempTextBoxValues));
objectsToRender.push(this.createAddButton(this.state.ipAddressAllow, tempTextBoxValues, "tempIpAddressAllowTextboxValue"));
objectsToRender.push(this.createTextEntryField("IP address block", "ipAddressBlockFilterClass", "tempIpAddressBlockTextboxValue", tempTextBoxValues));
objectsToRender.push(this.createAddButton(this.state.ipAddressBlock, tempTextBoxValues, "tempIpAddressBlockTextboxValue"));
objectsToRender.push(this.createTextEntryField("Transport port allow", "transportPortAllowFilterClass", "tempTransportPortAllowTextboxValue", tempTextBoxValues));
objectsToRender.push(this.createAddButton(this.state.transportPortAllow, tempTextBoxValues, "tempTransportPortAllowTextboxValue"));
objectsToRender.push(this.createTextEntryField("Transport port block", "transportPortBlockFilterClass", "tempTransportPortBlockTextboxValue", tempTextBoxValues));
objectsToRender.push(this.createAddButton(this.state.transportPortBlock, tempTextBoxValues, "tempTransportPortBlockTextboxValue"));
return objectsToRender.map(
object =>
<StackItem key={Math.random()} className="toolbar-item has-separator">
{object}
</StackItem>)
}
createAllowButton(key, itemToAllow, itemArrayToAllow) {
return <Button key={key + itemToAllow} onClick={() => {
itemArrayToAllow = this.removeItemOnce(itemArrayToAllow, itemToAllow)
this.autoSetFilters();
}
} type={Button.TYPE.PRIMARY} iconType={Button.ICON_TYPE.INTERFACE__SIGN__CLOSE}>
{key} {itemToAllow}
</Button>
}
createBlockButton(key, itemToBlock, itemArrayToBlock) {
return <Button key={key + itemToBlock} onClick={() => {
itemArrayToBlock = this.removeItemOnce(itemArrayToBlock, itemToBlock)
this.autoSetFilters()
}
} type={Button.TYPE.DESTRUCTIVE} iconType={Button.ICON_TYPE.INTERFACE__SIGN__CLOSE}>
{key} {itemToBlock}
</Button>
}
renderBottomStackButtons() {
let bottomButtons = []
bottomButtons.push(this.state.ipProtocolNumberAllow.map(
protocol => { return (this.createAllowButton("IP Proto: ", protocol, this.state.ipProtocolNumberAllow)) }
))
bottomButtons.push(this.state.ipProtocolNumberBlock.map(
protocol => { return (this.createBlockButton("IP Proto: ", protocol, this.state.ipProtocolNumberBlock)) }
))
bottomButtons.push(this.state.ipAddressAllow.map(
address => { return (this.createAllowButton("IP Addr: ", address, this.state.ipAddressAllow)) }
))
bottomButtons.push(this.state.ipAddressBlock.map(
address => { return (this.createBlockButton("IP Addr: ", address, this.state.ipAddressBlock)) }
))
bottomButtons.push(this.state.transportPortAllow.map(
portnum => { return (this.createAllowButton("Port #: ", portnum, this.state.transportPortAllow)) }
))
bottomButtons.push(this.state.transportPortBlock.map(
portnum => { return (this.createBlockButton("Port #: ", portnum, this.state.transportPortBlock)) }
))
return bottomButtons.map(
(button) => button
)
}
renderNrqlQueries(input) {
let output = "";
for (const property in input) {
output += property + ": " + input[property] + "\n\n"
}
return output
}
mergeObjects(objectsArray) {
let newObject = {}
if (objectsArray.length > 0) {
objectsArray.forEach((object) => {
// console.log(object);
if (object !== null && object !== undefined) {
Object.keys(object).forEach((key) => {
// console.log("Key: " + key); // key
// console.log(object[key]); // value
newObject[key] = object[key]
});
}
else {
// console.log("null or undefined object fed to mergeObject method!")
return null;
}
})
return newObject;
}
else {
return "objectsArray is empty in mergeObjects method."
}
}
fetchIpAddressInfoFromHost(ipAddress, hostDatabase) {
const baseUrl = "https://api.example.com"
return fetch(baseUrl + "/" + hostDatabase + "/" + ipAddress, {credentials: 'include'})
.then(response => {
// console.log("uncaught response: %o", response)
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.catch(error => {
// console.error('There has been a problem with your fetch operation:', error);
return {error_stack: error.stack}
});
//console.log("caught response: %o", response)
//fetchIpAddress();
}
async checkIpForMaliciousActivity(ipAddress) {
let accumulatedResponses = [];
await this.fetchIpAddressInfoFromHost(ipAddress, "virustotal")
.then((response) => {
accumulatedResponses.push(response);
});
await this.fetchIpAddressInfoFromHost(ipAddress, "alienvault")
.then((response) => {
accumulatedResponses.push(response);
});
await this.fetchIpAddressInfoFromHost(ipAddress, "reverse")
.then((response) => {
accumulatedResponses.push(response);
});
// console.log("Accumulated Responses:" + accumulatedResponses);
// console.log(this.mergeObjects(accumulatedResponses))
this.setIPInfo(this.mergeObjects(accumulatedResponses));
}
renderIpAddressInfo() {
let objectsToRender = [];
let ipAddrRenderResult;
// console.log(this.state.ipInfoJson);
if ((this.state.ipInfoJson !== null && this.state.ipInfoJson !== undefined) && Object.keys(this.state.ipInfoJson).length >= 0) {
Object.keys(this.state.ipInfoJson).forEach((key) => {
// console.log(key);
objectsToRender.push(<p>{key}: {this.state.ipInfoJson[key]}</p>);
})
ipAddrRenderResult = <div>{ objectsToRender }</div>
if ('error_stack' in this.state.ipInfoJson){
// console.log("Error stack found!");
ipAddrRenderResult = <div>There was an error connecting to the API. Try clicking through <a href="https://netflowapi.service.newrelic.com">this link</a> first then retry here.</div>
}
}
else {
ipAddrRenderResult = <div>Click an IP address to get started!</div>
}
return ipAddrRenderResult;
}
setAPIKSKey(key) {
UserStorageMutation.mutate({
actionType: UserStorageMutation.ACTION_TYPE.WRITE_DOCUMENT,
collection: 'netflowapicollection',
documentId: 'netflowapiuserapikeys',
document: {apiks: key},
});
}
openEntity() {
const { entityGuid, appName, trafficDirectionFilter, ipProtocolNumberAllow, ipAddressAllow, ipProtocolNumberBlock, ipAddressBlock, transportPortAllow, transportPortBlock } = this.state;
nerdlet.setUrlState({ entityGuid, appName, trafficDirectionFilter, ipProtocolNumberAllow, ipAddressAllow, ipProtocolNumberBlock, ipAddressBlock, transportPortAllow, transportPortBlock }); //eslint-disable-line
navigation.openEntity(entityGuid); //eslint-disable-line
}
render() {
var { entityGuid, //eslint-disable-line
appName, //eslint-disable-line
trafficDirectionFilter,
ipProtocolNumberAllow,
ipAddressAllow,
ipProtocolNumberBlock,
ipAddressBlock,
transportPortAllow,
transportPortBlock } = this.state;
const nrql_broadtraffic = `SELECT rate(sum(octetDeltaCount * 64000), 1 second) as 'bps' FROM ipfix TIMESERIES`;
const nrql_iptraffic_source = `SELECT count(sourceIPv4Address) as 'Number of logs' FROM ipfix FACET sourceIPv4Address`;
const nrql_iptraffic_dest = `SELECT count(destinationIPv4Address) as 'Number of logs' FROM ipfix FACET destinationIPv4Address`;
const nrql_iptraffic_ipproto = `SELECT count(protocolIdentifier) as 'IP protocol' FROM ipfix LOOKUP ip_protocol_numbers FACET protocolIdentifier`;
const nrql_transport_port = `SELECT count(destinationTransportPort) FROM ipfix FACET destinationTransportPort`;
var nrql_ipprotonum_filter = " ";
if (ipProtocolNumberAllow != []) {
for (let i = 0; i < ipProtocolNumberAllow.length; i++) {
if (i === 0) {
nrql_ipprotonum_filter += " WHERE protocolIdentifier = " + ipProtocolNumberAllow[i];
}
else {
nrql_ipprotonum_filter += " OR protocolIdentifier = " + ipProtocolNumberAllow[i];
}
}
}
if (ipProtocolNumberBlock != []) {
for (let i = 0; i < ipProtocolNumberBlock.length; i++) {
if (i === 0 && nrql_ipprotonum_filter === " ") {
nrql_ipprotonum_filter += " WHERE protocolIdentifier != " + ipProtocolNumberBlock[i];
}
else {
nrql_ipprotonum_filter += " AND protocolIdentifier != " + ipProtocolNumberBlock[i];
}
}
}
// console.log("nrql_ipprotonum_filter: %s", nrql_ipprotonum_filter)
var nrql_transportnum_filter = " ";
if (transportPortAllow != []) {
for (let i = 0; i < transportPortAllow.length; i++) {
if (i === 0) {
nrql_transportnum_filter += " WHERE destinationTransportPort = " + transportPortAllow[i];
}
else {
nrql_transportnum_filter += " OR destinationTransportPort = " + transportPortAllow[i];
}
}
}
if (transportPortBlock != []) {
for (let i = 0; i < transportPortBlock.length; i++) {
if (i === 0 && nrql_transportnum_filter === " ") {
nrql_transportnum_filter += " WHERE destinationTransportPort != " + transportPortBlock[i];
}
else {
nrql_transportnum_filter += " AND destinationTransportPort != " + transportPortBlock[i];
}
}
}
// console.log("nrql_transportnum_filter: %s", nrql_transportnum_filter)
var nrql_ipaddr_filter = " ";
if (ipAddressAllow != []) {
for (let i = 0; i < ipAddressAllow.length; i++) {
if (i === 0) {
nrql_ipaddr_filter += " WHERE (destinationIPv4Address LIKE '" + ipAddressAllow[i] + "' OR sourceIPv4Address LIKE '" + ipAddressAllow[i] + "')"
}
else {
nrql_ipaddr_filter += " AND (destinationIPv4Address LIKE '" + ipAddressAllow[i] + "' OR sourceIPv4Address LIKE '" + ipAddressAllow[i] + "')"
}
}
}
if (ipAddressBlock != []) {
for (let i = 0; i < ipAddressBlock.length; i++) {
let prefix = "";
if (i === 0 && nrql_ipaddr_filter === " ") {
prefix = " WHERE "
} else {
prefix = " AND "
}
if (trafficDirectionFilter === "Combined") {
nrql_ipaddr_filter += prefix + "(destinationIPv4Address NOT LIKE '" + ipAddressBlock[i] + "' AND sourceIPv4Address NOT LIKE '" + ipAddressBlock[i] + "')"
} else if (trafficDirectionFilter === "Inbound") {
nrql_ipaddr_filter += prefix + "(sourceIPv4Address NOT LIKE '" + ipAddressBlock[i] + "')"
} else if (trafficDirectionFilter === "Outbound") {
nrql_ipaddr_filter += prefix + "(destinationIPv4Address NOT LIKE '" + ipAddressBlock[i] + "')"
}
}
}
// console.log("nrql_ipaddr_filter: %s", nrql_ipaddr_filter)
var nrql_trafficdir_filter = " ";
if (trafficDirectionFilter != "Combined") {
if (trafficDirectionFilter == "Inbound") {
nrql_trafficdir_filter = " WHERE bgpSourceAsNumber NOT IN (1, 2, 3, 4)"
} else if (trafficDirectionFilter == "Outbound") {
nrql_trafficdir_filter = " WHERE bgpSourceAsNumber IN (1, 2, 3, 4)"
}
}
return (
<PlatformStateContext.Consumer>
{(platformUrlState) => {
//console.debug here for learning purposes
console.debug("platformUrlState: %o", platformUrlState); //eslint-disable-line
let since = "";
if (platformUrlState.hasOwnProperty('timeRange') && (typeof platformUrlState.timeRange !== 'undefined')) {
if (platformUrlState.timeRange.hasOwnProperty('begin_time') && platformUrlState.timeRange.begin_time && platformUrlState.timeRange.hasOwnProperty('end_time') && platformUrlState.timeRange.end_time) {
// const { begin_time, end_time, duration } = platformUrlState.timeRange;
// console.debug("begin_time, end_time, duration: %o, %o, %o", begin_time, end_time, duration)
since = ` SINCE ${platformUrlState.timeRange.begin_time} UNTIL ${platformUrlState.timeRange.end_time}`;
// console.debug("since: %o", since);
} else if (platformUrlState.timeRange.duration) {
// const { begin_time, end_time, duration } = platformUrlState.timeRange;
// console.debug("begin_time, end_time, duration: %o, %o, %o", begin_time, end_time, duration)
since = ` SINCE ${platformUrlState.timeRange.duration/60/1000} MINUTES AGO`;
// console.debug("since: %o", since);
}
}
// console.debug("trafficDirectionFilter: %s", trafficDirectionFilter);
let tempCustomNrqlTextBox = ""; //eslint-disable-line
// let tempAPIKSTextBox = "";
{/* created a JS object to store these values,
objects in JS are passed by reference when passed as a function parameter*/}
let tempTextBoxValues = {
tempIpProtocolAllowTextboxValue: "",
tempIpProtocolBlockTextboxValue: "",
tempIpAddressAllowTextboxValue: "",
tempIpAddressBlockTextboxValue: "",
tempTransportPortAllowTextboxValue: "",
tempTransportPortBlockTextboxValue: ""
};
let ipAddrRenderResult = null;
ipAddrRenderResult = this.renderIpAddressInfo();
let finishedNrqlQueries = {
'trafficChart': nrql_broadtraffic+nrql_trafficdir_filter+nrql_ipprotonum_filter+nrql_ipaddr_filter+nrql_transportnum_filter+since,
'ipProtocol': nrql_iptraffic_ipproto+nrql_ipprotonum_filter+nrql_trafficdir_filter+nrql_ipaddr_filter+nrql_transportnum_filter+since,
'transportLayerPorts': nrql_transport_port+nrql_ipprotonum_filter+nrql_trafficdir_filter+nrql_ipaddr_filter+nrql_transportnum_filter+since,
'sourceIPs': nrql_iptraffic_source+nrql_ipprotonum_filter+nrql_trafficdir_filter+nrql_ipaddr_filter+nrql_transportnum_filter+since,
'destinationIPs' : nrql_iptraffic_dest+nrql_ipprotonum_filter+nrql_trafficdir_filter+nrql_ipaddr_filter+nrql_transportnum_filter+since
}
return (
<div className="full-height">
<Stack
className="toolbar-container"
fullWidth
gapType={Stack.GAP_TYPE.NONE}
horizontalType={Stack.HORIZONTAL_TYPE.FILL_EVENLY}
verticalType={Stack.VERTICAL_TYPE.FILL}
>
<StackItem className="toolbar-section1">
<Stack
gapType={Stack.GAP_TYPE.NONE}
fullWidth
verticalType={Stack.VERTICAL_TYPE.FILL}
>
<StackItem className="toolbar-item has-separator">
<Select onChange={(evt, value) => this.setFilters(value, ipProtocolNumberAllow, ipAddressAllow, ipProtocolNumberBlock, ipAddressBlock, transportPortAllow, transportPortBlock)} label="Traffic Direction" className="trafficDirectionFilterclass" value={trafficDirectionFilter}>
<SelectItem value="Inbound">Inbound</SelectItem>
<SelectItem value="Outbound">Outbound</SelectItem>
<SelectItem value="Combined">Combined</SelectItem>
</Select>
</StackItem>
{ this.populateTopToolbarStack(tempTextBoxValues) }
</Stack>
</StackItem>
</Stack>
<Stack
className="toolbar-container-2"
fullWidth
gapType={Stack.GAP_TYPE.NONE}
horizontalType={Stack.HORIZONTAL_TYPE.FILL_EVENLY}
verticalType={Stack.VERTICAL_TYPE.FILL}
>
<StackItem className="toolbar-section2">
<Stack
gapType={Stack.GAP_TYPE.NONE}
fullWidth
verticalType={Stack.VERTICAL_TYPE.FILL}
>
<StackItem>
{ this.renderBottomStackButtons() }
</StackItem>
</Stack>
</StackItem>
</Stack>
<Grid
className="primary-grid full-height"
spacingType={[
Grid.SPACING_TYPE.LARGE,
Grid.SPACING_TYPE.LARGE,
Grid.SPACING_TYPE.NONE,
Grid.SPACING_TYPE.LARGE
]}
>
<GridItem columnSpan={6}>
<main className="primary-content full-height">
<HeadingText type={HeadingText.TYPE.HEADING_3}>Traffic Rate (bps)</HeadingText>
<LineChart accountId={1} fullHeight={true} fullWidth={true} query={finishedNrqlQueries['trafficChart']} />
</main>
</GridItem>
<GridItem columnSpan={3}>
<main className="primary-content full-height">
<HeadingText type={HeadingText.TYPE.HEADING_3}>IP Protocols</HeadingText>
<BarChart accountId={1} fullHeight={true} fullWidth={true} query={finishedNrqlQueries['ipProtocol']} />
</main>
</GridItem>
<GridItem columnSpan={3}>
<main className="primary-content full-height">
<HeadingText type={HeadingText.TYPE.HEADING_3}>Transport Layer Ports</HeadingText>
<BarChart accountId={1} fullHeight={true} fullWidth={true} query={finishedNrqlQueries['transportLayerPorts']} />
</main>
</GridItem>
<GridItem columnSpan={5}>
<main className="primary-content">
<TableChart accountId={1} fullWidth={true} onClickTable={(i, j) => {this.checkIpForMaliciousActivity(j.sourceIPv4Address)}} query={finishedNrqlQueries['sourceIPs']} />
</main>
</GridItem>
<GridItem columnSpan={5}>
<main className="primary-content">
<TableChart accountId={1} fullWidth={true} onClickTable={(i, j) => {this.checkIpForMaliciousActivity(j.destinationIPv4Address)}} query={finishedNrqlQueries['destinationIPs']} />
</main>
</GridItem>
<GridItem columnSpan={2}>
<main className="primary-content">
<Card>
<CardHeader title="IP address information" />
<CardBody>
{ipAddrRenderResult}
</CardBody>
</Card>
</main>
</GridItem>
<GridItem columnSpan={6}>
<main className="primary-content">
<TextField
multiline
label="NRQL queries"
value={this.renderNrqlQueries(finishedNrqlQueries)}
/>
</main>
</GridItem>
<GridItem columnSpan={6}>
<main className="primary-content">
<h3>Before using this nerdlet, please click through <a href="https://api.example.com">this link</a> to authenticate to the backend API</h3>
</main>
</GridItem>
</Grid>
</div>
);
}}
</PlatformStateContext.Consumer>
)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment