Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
151 changes: 151 additions & 0 deletions redash/query_runner/newrelic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import json
import logging
from collections import OrderedDict
from urllib.parse import quote_from_bytes

from redash.query_runner import *
from redash.utils import json_dumps, json_loads


logger = logging.getLogger(__name__)

# TODO: make this more general and move into __init__.py
class ResultSet(object):
def __init__(self):
self.columns = OrderedDict()
self.rows = []

def add_row(self, row):
for key in row.keys():
self.add_column(key)

self.rows.append(row)

def add_column(self, column, column_type=TYPE_STRING):
if column not in self.columns:
self.columns[column] = {
"name": column,
"type": column_type,
"friendly_name": column,
}

def to_json(self):
return json_dumps({"rows": self.rows, "columns": list(self.columns.values())})

def merge(self, set):
self.rows = self.rows + set.rows


def pct_change(current, previous):
diff = current - previous
change = 0
try:
if diff > 0:
change = (diff / current) * 100
elif diff < 0:
diff = previous - current
change = -((diff / current) * 100)
except ZeroDivisionError:
return float("inf")
return float("{:.2f}".format(change))


def parse_comparision(data):
nested_data = data.get("data").get("actor").get("account").get("nrql").get("results")
results = ResultSet()
data_dict = {}
for rows in nested_data:
try:
data_dict[rows["comparison"]] = rows["count"]
except (KeyError, IndexError) as err:
logger.error(f"Error adding data to dictionary, err: {err}")

if len(data_dict) >= 1:
logger.info(f"Data Dictionary: {data_dict}")
data_dict['change'] = pct_change(data_dict.get('current'), data_dict.get('previous'))

results.add_row(data_dict)
return results


def parse_count(data):
nested_data = data.get("data").get("actor").get("account").get("nrql").get("results")[0]
key_name = list(nested_data.keys())[0]
data_count = list(nested_data.values())[0]
results = ResultSet()
results.add_row({key_name: data_count})
return results


class NewRelicGQL(BaseHTTPQueryRunner):
should_annotate_query = False
response_error = "NewRelic returned unexpected status code"

@classmethod
def configuration_schema(cls):
return {
"type": "object",
"properties": {
"nr_account_id": {"type": "string", "title": "NewRelic Account ID"},
"url": {"type": "string", "title": "API URL"},
"token": {"type": "string", "title": "Security Token"},
},
"required": ["nr_account_id", "url", "token"],
"secret": ["token"],
"order": [
"nr_account_id",
"url",
"token",
]
}

@classmethod
def name(cls):
return "NewRelic (GraphQL)"

def test_connection(self):
nr_account_id = str("{}".format(self.configuration["nr_account_id"]))
qraphql_test_query = '{actor {account(id: ' + nr_account_id +') {nrql(query: "SELECT 1") {results}}}}'
testQuery = {"queryType": "count", "query": qraphql_test_query}
try:
response = self.run_query(query=json.dumps(testQuery), user="test")
except Exception as err:
logger.info(f"Raised Exception: {err}")
response = None
if response is None:
raise Exception("Failed describing objects.")
pass

def run_query(self, query, user):
nr_url = "{}".format(self.configuration["url"])
nr_token = "{}".format(self.configuration["token"])
nr_account_id = "{}".format(self.configuration["nr_account_id"])
headers = {
"Content-Type": "application/json",
"API-Key": "{}".format(nr_token),
}

query = json_loads(query)
query_type = query.pop("queryType", "count")
nrql_query = query.pop("nrql", None)
if not nrql_query or not nr_account_id:
return None, None

qraphql_query = '{actor {account(id: ' + nr_account_id +') {nrql(query: "' + nrql_query + '") {results}}}}'
payload = {"query":qraphql_query}
response, error = self.get_response(nr_url, http_method="post", data=json.dumps(payload), headers=headers)

if error is not None:
return None, error
data = response.json()

if query_type == "count":
results = parse_count(data)

if query_type == "comparison":
results = parse_comparision(data)

return results.to_json(), None


register(NewRelicGQL)
1 change: 1 addition & 0 deletions redash/settings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,7 @@ def email_server_is_configured():
"redash.query_runner.mssql_odbc",
"redash.query_runner.memsql_ds",
"redash.query_runner.mapd",
"redash.query_runner.newrelic",
"redash.query_runner.jql",
"redash.query_runner.google_analytics",
"redash.query_runner.axibase_tsd",
Expand Down
31 changes: 31 additions & 0 deletions viz-lib/src/visualizations/counter/Editor/GeneralSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,37 @@ export default function GeneralSettings({ options, data, visualizationName, onOp
/>
</Section>

{/* @ts-expect-error ts-migrate(2745) FIXME: This JSX tag's 'children' prop expects type 'never... Remove this comment to see the full error message */}
<Section>
<Select
layout="horizontal"
label="Percent Change Column Name"
data-test="Counter.General.PercentChangeColumn"
defaultValue={options.percentColName}
onChange={(percentColName: any) => onOptionsChange({ percentColName })}>
{/* @ts-expect-error ts-migrate(2339) FIXME: Property 'Option' does not exist on type '({ class... Remove this comment to see the full error message */}
<Select.Option value="">No %change value</Select.Option>
{map(data.columns, col => (
// @ts-expect-error ts-migrate(2339) FIXME: Property 'Option' does not exist on type '({ class... Remove this comment to see the full error message
<Select.Option key={col.name} data-test={"Counter.General.PercentChangeColumn." + col.name}>
{col.name}
{/* @ts-expect-error ts-migrate(2339) FIXME: Property 'Option' does not exist on type '({ class... Remove this comment to see the full error message */}
</Select.Option>
))}
</Select>
</Section>

{/* @ts-expect-error ts-migrate(2745) FIXME: This JSX tag's 'children' prop expects type 'never... Remove this comment to see the full error message */}
<Section>
<InputNumber
layout="horizontal"
label="Percent Change Row Number"
data-test="Counter.General.PercentChangeRowNumber"
defaultValue={options.percentRowNumber}
onChange={(percentRowNumber: any) => onOptionsChange({ percentRowNumber })}
/>
</Section>

{/* @ts-expect-error ts-migrate(2745) FIXME: This JSX tag's 'children' prop expects type 'never... Remove this comment to see the full error message */}
<Section>
{/* @ts-expect-error ts-migrate(2745) FIXME: This JSX tag's 'children' prop expects type 'never... Remove this comment to see the full error message */}
Expand Down
16 changes: 12 additions & 4 deletions viz-lib/src/visualizations/counter/Renderer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,18 +59,26 @@ export default function Renderer({ data, options, visualizationName }: any) {
targetValueTooltip,
// @ts-expect-error ts-migrate(2339) FIXME: Property 'counterLabel' does not exist on type '{}... Remove this comment to see the full error message
counterLabel,
// @ts-expect-error ts-migrate(2339) FIXME: Property 'percentValue' does not exist on type '{}... Remove this comment to see the full error message
percentValue,
// @ts-expect-error ts-migrate(2339) FIXME: Property 'percentValueTooltip' does not exist on ty... Remove this comment to see the full error message
percentValueTooltip
} = getCounterData(data.rows, options, visualizationName);
return (
<div
className={cx("counter-visualization-container", {
"trend-positive": showTrend && trendPositive,
"trend-negative": showTrend && !trendPositive,
})}>
className={cx("counter-visualization-container",
{"trend-positive": showTrend && trendPositive, "trend-negative": showTrend && !trendPositive,}
)}>
{/* @ts-expect-error ts-migrate(2322) FIXME: Type 'Dispatch<SetStateAction<null>>' is not assig... Remove this comment to see the full error message */}
<div className="counter-visualization-content" ref={setContainer}>
<div style={getCounterStyles(scale)}>
<div className="counter-visualization-value" title={counterValueTooltip}>
{counterValue}
{percentValue && (
<div className="counter-visualization-value counter-visualization-percent" title={percentValueTooltip}>
{percentValue}%
</div>
)}
</div>
{targetValue && (
<div className="counter-visualization-target" title={targetValueTooltip}>
Expand Down
7 changes: 7 additions & 0 deletions viz-lib/src/visualizations/counter/render.less
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,16 @@
color: #ccc;
}

.counter-visualization-percent {
font-size: 0.25em;
font-weight: bold;
}

.counter-visualization-label {
font-size: 0.5em;
display: block;
}

}

&.trend-positive .counter-visualization-value {
Expand All @@ -44,4 +50,5 @@
&.trend-negative .counter-visualization-value {
color: #d9534f;
}

}
2 changes: 2 additions & 0 deletions viz-lib/src/visualizations/counter/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ describe("Visualizations -> Counter -> Utils", () => {
counterLabel: "Visualisation Name",
counterValue: "",
targetValue: null,
percentValue: null,
percentValueTooltip: "",
counterValueTooltip: "",
targetValueTooltip: "",
},
Expand Down
17 changes: 17 additions & 0 deletions viz-lib/src/visualizations/counter/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export function getCounterData(rows: any, options: any, visualizationName: any)
if (rowsCount > 0 || options.countRow) {
const counterColName = options.counterColName;
const targetColName = options.targetColName;
const percentColName = options.percentColName;

// @ts-expect-error ts-migrate(2339) FIXME: Property 'counterLabel' does not exist on type '{}... Remove this comment to see the full error message
result.counterLabel = options.counterLabel || visualizationName;
Expand Down Expand Up @@ -108,10 +109,26 @@ export function getCounterData(rows: any, options: any, visualizationName: any)
result.targetValue = null;
}

if (percentColName) {
const percentRowNumber = getRowNumber(options.percentRowNumber, rowsCount);
// @ts-expect-error ts-migrate(2339) FIXME: Property 'percentValue' does not exist on type '{}'... Remove this comment to see the full error message
result.percentValue = rows[percentRowNumber][percentColName];
// @ts-expect-error ts-migrate(2339) FIXME: Property 'counterValue' does not exist on type '{}... Remove this comment to see the full error message
if (Number.isFinite(result.percentValue)) {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'counterValue' does not exist on type '{}... Remove this comment to see the full error message
const percentDelta = result.percentValue;
}
} else {
// @ts-expect-error ts-migrate(2339) FIXME: Property 'targetValue' does not exist on type '{}'... Remove this comment to see the full error message
result.percentValue = null;
}

// @ts-expect-error ts-migrate(2339) FIXME: Property 'counterValueTooltip' does not exist on t... Remove this comment to see the full error message
result.counterValueTooltip = formatTooltip(result.counterValue, options.tooltipFormat);
// @ts-expect-error ts-migrate(2339) FIXME: Property 'targetValueTooltip' does not exist on ty... Remove this comment to see the full error message
result.targetValueTooltip = formatTooltip(result.targetValue, options.tooltipFormat);
// @ts-expect-error ts-migrate(2339) FIXME: Property 'percentValueTooltip' does not exist on ty... Remove this comment to see the full error message
result.percentValueTooltip = formatTooltip(result.percentValue, options.tooltipFormat);

// @ts-expect-error ts-migrate(2339) FIXME: Property 'counterValue' does not exist on type '{}... Remove this comment to see the full error message
result.counterValue = formatValue(result.counterValue, options);
Expand Down