Skip to content

Commit ae3120f

Browse files
add simple scripting with javascript (#11)
1 parent b24934c commit ae3120f

File tree

8 files changed

+301
-35
lines changed

8 files changed

+301
-35
lines changed

package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,8 @@
6868
"@grafana/ui": "^11.2.0",
6969
"react": "18.2.0",
7070
"react-dom": "18.2.0",
71+
"shadowrealm-api": "^0.8.3",
7172
"tslib": "2.5.3"
7273
},
7374
"packageManager": "[email protected]"
74-
}
75+
}

src/components/QueryEditor.tsx

Lines changed: 33 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,20 @@
11
import React, { ChangeEvent, useRef, useState } from "react";
2-
import { Button, CodeEditor, Divider, Field, InlineField, InlineFieldRow, InlineSwitch, Input, Select } from "@grafana/ui";
2+
import {
3+
ActionMeta,
4+
Button,
5+
CodeEditor,
6+
Divider,
7+
Field,
8+
InlineField,
9+
InlineFieldRow,
10+
Input,
11+
Select,
12+
13+
} from "@grafana/ui";
314
import { QueryEditorProps, SelectableValue } from "@grafana/data";
415
import { DataSource } from "../datasource";
516
import { MongoDataSourceOptions, MongoQuery, QueryLanguage, QueryType, DEFAULT_QUERY } from "../types";
6-
import { parseJsQuery, validateJsonQueryText } from "../utils";
17+
import {parseJsQuery, parseJsQueryLegacy, validateJsonQueryText} from "../utils";
718
import * as monacoType from "monaco-editor/esm/vs/editor/editor.api";
819

920
type Props = QueryEditorProps<DataSource, MongoQuery, MongoDataSourceOptions>;
@@ -25,10 +36,18 @@ export function QueryEditor({ query, onChange, onRunQuery }: Props) {
2536
const codeEditorRef = useRef<monacoType.editor.IStandaloneCodeEditor | null>(null);
2637
const [queryTextError, setQueryTextError] = useState<string | null>(null);
2738

39+
const optionsLanguage = [
40+
{ label: "JSON", value: QueryLanguage.JSON},
41+
{ label: "JavaScript", value: QueryLanguage.JAVASCRIPT, description: "javascript legacy" },
42+
{ label: "JavaScriptShadow", value: QueryLanguage.JAVASCRIPT_SHADOW, description: "javascript with evaluation" }
43+
];
44+
2845
const onQueryTextChange = (queryText: string) => {
29-
if (query.queryLanguage === QueryLanguage.JAVASCRIPT) {
30-
const { collection, error } = parseJsQuery(queryText);
31-
onChange({ ...query, collection: collection, queryText: queryText });
46+
if (query.queryLanguage === QueryLanguage.JAVASCRIPT || query.queryLanguage === QueryLanguage.JAVASCRIPT_SHADOW) {
47+
// parse the JavaScript query
48+
const { error, collection } = query.queryLanguage === QueryLanguage.JAVASCRIPT_SHADOW ? parseJsQuery(queryText) : parseJsQueryLegacy(queryText);
49+
// let the same query text as it is
50+
onChange({ ...query, queryText: queryText, ...(collection ? {collection} : {}) });
3251
setQueryTextError(error);
3352
if (!error) {
3453
onRunQuery();
@@ -43,13 +62,8 @@ export function QueryEditor({ query, onChange, onRunQuery }: Props) {
4362
}
4463
};
4564

46-
const onQueryLanguageChange = (e: React.FormEvent<HTMLInputElement>) => {
47-
if (e.currentTarget.checked) {
48-
onChange({ ...query, queryLanguage: QueryLanguage.JAVASCRIPT });
49-
50-
} else {
51-
onChange({ ...query, queryLanguage: QueryLanguage.JSON });
52-
}
65+
const onQueryLanguageChange = (value: SelectableValue<string>, actionMeta: ActionMeta) => {
66+
onChange({ ...query, queryLanguage: value.value });
5367
};
5468

5569
const onQueryTypeChange = (sv: SelectableValue<string>) => {
@@ -80,18 +94,18 @@ export function QueryEditor({ query, onChange, onRunQuery }: Props) {
8094
<InlineField label="Query Type">
8195
<Select id="query-editor-query-type" options={queryTypes} value={query.queryType || QueryType.TIMESERIES} onChange={onQueryTypeChange}></Select>
8296
</InlineField>
83-
{query.queryLanguage === QueryLanguage.JSON && <InlineField label="Collection" tooltip="Enter the collection to query"
84-
error="Please enter the collection" invalid={!query.collection}>
97+
<InlineField label="Collection" tooltip="Enter the collection to query"
98+
error="Please enter the collection" invalid={!query.collection}>
8599
<Input id="query-editor-collection" onChange={onCollectionChange} value={query.collection} required />
86-
</InlineField>}
100+
</InlineField>
87101
</InlineFieldRow>
88102
<Divider />
89-
<InlineField label="Use JavaScript Query">
90-
<InlineSwitch id="query-editor-use-js-query" value={query.queryLanguage === QueryLanguage.JAVASCRIPT} onChange={onQueryLanguageChange} />
103+
<InlineField label="Query language">
104+
<Select id="query-editor-use-js-query" onChange={onQueryLanguageChange} options={optionsLanguage} value={query.queryLanguage} />
91105
</InlineField>
92-
<Field label="Query Text" description={`Enter the Mongo Aggregation Pipeline (${query.queryLanguage === QueryLanguage.JSON ? "JSON" : "JavaScript"})`}
106+
<Field label="Query Text" description={`Enter the Mongo Aggregation Pipeline (${query.queryLanguage})`}
93107
error={queryTextError} invalid={queryTextError != null}>
94-
<CodeEditor onEditorDidMount={onCodeEditorDidMount} width="100%" height={300} language={query.queryLanguage || ""}
108+
<CodeEditor onEditorDidMount={onCodeEditorDidMount} width="100%" height={300} language={query.queryLanguage === QueryLanguage.JAVASCRIPT || query.queryLanguage === QueryLanguage.JAVASCRIPT_SHADOW ? "javascript" : "json"}
95109
onBlur={onQueryTextChange} value={query.queryText || ""} showMiniMap={false} showLineNumbers={true} />
96110
</Field>
97111
<Button onClick={onFormatQueryText}>Format</Button>

src/components/QueryHelper.tsx

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,36 @@
11
import React from "react";
22
import { QueryEditorHelpProps } from "@grafana/data";
3+
import {Divider, Text} from "@grafana/ui";
34

45
export function QueryHelper(_props: QueryEditorHelpProps) {
5-
return <p>TODO</p>;
6-
};
6+
return (
7+
<div>
8+
<Text element="h1" color="primary">
9+
Query Language Types and Differences
10+
</Text>
11+
12+
<Divider></Divider>
13+
<div>
14+
<p>
15+
<Text element="span" weight="bold" color="info">
16+
JSON
17+
</Text>
18+
: JSON aggregate of mongodb query.
19+
</p>
20+
<p>
21+
<Text element="span" weight="bold" color="info">
22+
JavaScript
23+
</Text>
24+
:JavaScript aggregate of mongodb query for the legacy support
25+
</p>
26+
<p>
27+
<Text element="span" weight="bold" color="info">
28+
JavaScript Shadow
29+
</Text>
30+
: JavaScript function that return aggregate of mongodb query with evaluation
31+
support.
32+
</p>
33+
</div>
34+
</div>
35+
);
36+
}

src/datasource.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { DataSourceInstanceSettings, CoreApp, ScopedVars, DataQueryRequest, DataQueryResponse } from "@grafana/data";
22
import { DataSourceWithBackend, getTemplateSrv } from "@grafana/runtime";
3-
import { parseJsQuery, datetimeToJson, getBucketCount } from "./utils";
3+
import {parseJsQuery, datetimeToJson, getBucketCount, parseJsQueryLegacy} from "./utils";
44
import { MongoQuery, MongoDataSourceOptions, DEFAULT_QUERY, QueryLanguage } from "./types";
55
import { Observable } from "rxjs";
66

@@ -26,18 +26,25 @@ export class DataSource extends DataSourceWithBackend<MongoQuery, MongoDataSourc
2626
}
2727

2828
query(request: DataQueryRequest<MongoQuery>): Observable<DataQueryResponse> {
29-
const queries = request.targets.map(query => {
29+
const queries = request.targets.map((query) => {
3030
let queryText = query.queryText!;
31-
if (query.queryLanguage === QueryLanguage.JAVASCRIPT) {
32-
const { jsonQuery } = parseJsQuery(queryText);
31+
if (query.queryLanguage === QueryLanguage.JAVASCRIPT || query.queryLanguage === QueryLanguage.JAVASCRIPT_SHADOW) {
32+
const { jsonQuery } =
33+
query.queryLanguage === QueryLanguage.JAVASCRIPT_SHADOW
34+
? parseJsQuery(queryText)
35+
: parseJsQueryLegacy(queryText);
3336
queryText = jsonQuery!;
3437
}
3538

3639
return {
3740
...query,
38-
queryText: queryText.replaceAll(/"\$from"/g, datetimeToJson(request.range.from))
41+
queryText: queryText
42+
.replaceAll(/"\$from"/g, datetimeToJson(request.range.from))
3943
.replaceAll(/"\$to"/g, datetimeToJson(request.range.to))
40-
.replaceAll(/"\$dateBucketCount"/g, getBucketCount(request.range.from, request.range.to, request.intervalMs).toString())
44+
.replaceAll(
45+
/"\$dateBucketCount"/g,
46+
getBucketCount(request.range.from, request.range.to, request.intervalMs).toString()
47+
),
4148
};
4249
});
4350

src/types.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ export const QueryType = {
1515

1616
export const QueryLanguage = {
1717
JSON: "json",
18-
JAVASCRIPT: "javascript"
18+
JAVASCRIPT: "javascript",
19+
JAVASCRIPT_SHADOW: "javascriptShadow"
1920
};
2021

2122

src/utils.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { DateTime } from "@grafana/data";
22
import { JsQueryResult } from "types";
3+
import shadow from "shadowrealm-api";
4+
import {getTemplateSrv} from "@grafana/runtime";
35

46
export function validateJsonQueryText(queryText?: string): string | null {
57
if (!queryText) {
@@ -19,7 +21,7 @@ export function validateJsonQueryText(queryText?: string): string | null {
1921
}
2022
}
2123

22-
export function parseJsQuery(queryText: string): JsQueryResult {
24+
export function parseJsQueryLegacy(queryText: string): JsQueryResult {
2325
const regex = /^db\.(.+)\.aggregate\((.+)\)$/;
2426
const match = queryText.trim().replace(/(;$)/g, "").replace(/(\r\n|\n|\r)/gm, "")
2527
.match(regex);
@@ -39,14 +41,32 @@ export function parseJsQuery(queryText: string): JsQueryResult {
3941
}
4042
}
4143

42-
export function validateJsQueryText(queryText?: string): string | null {
43-
if (!queryText) {
44-
return "Please enter the query";
44+
export function parseJsQuery(queryText: string): JsQueryResult {
45+
// use shadow realm to evaluate the JavaScript query
46+
const realm = new shadow();
47+
try {
48+
// replace the template variables in the query
49+
const script = getTemplateSrv().replace(queryText);
50+
// realm.evaluate will execute the JavaScript code and return the result
51+
const result = realm.evaluate(`
52+
const fn = ${script}
53+
const result = fn()
54+
JSON.stringify(result)
55+
`) as string;
56+
return {
57+
jsonQuery: result,
58+
error: null
59+
};
60+
}catch (e: Error | any) {
61+
// if there is an error, return the error message
62+
return {
63+
error: e?.message
64+
};
4565
}
46-
const { error } = parseJsQuery(queryText);
47-
return error;
4866
}
4967

68+
69+
5070
export function datetimeToJson(datetime: DateTime) {
5171
return JSON.stringify({
5272
$date: {

0 commit comments

Comments
 (0)