diff --git a/README.md b/README.md index 67234d3..9dcee9c 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,19 @@ If you are using IntelliJ IDEA Ultimate, make sure go to "Language & Frameworks If you are using VS Code, this is a good read: https://github.com/golang/vscode-go/blob/master/docs/gopath.md +### Common Errors During Development + +* `Watchpack Error (watcher): Error: ENOSPC: System limit for number of file watchers reached, watch` + * https://stackoverflow.com/a/55543310/5434860 + +## Dependency Notes + +This section contains notes about dependencies. + +* `graphql-ws` is not actually required by us, but this issue is unresolved so that's why we include it + * https://github.com/graphql/graphiql/issues/2405#issuecomment-1469851608 (yes as of writing it says it's closed, but it's not) + * It's not a bad thing that we include this dependency because it gives us a couple of types that we end up using + ## To-Do * Add metrics to backend component: https://grafana.com/developers/plugin-tools/create-a-plugin/extend-a-plugin/add-logs-metrics-traces-for-backend-plugins#implement-metrics-in-your-plugin diff --git a/package-lock.json b/package-lock.json index a9fdd1e..bf4b801 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,8 +14,9 @@ "@grafana/runtime": "10.0.3", "@grafana/schema": "10.0.3", "@grafana/ui": "10.0.3", - "graphiql": "^3.0.10", + "graphiql": "^3.1.0", "graphql": "^16.8.1", + "graphql-ws": "^5.14.3", "react": "18.2.0", "react-dom": "18.2.0", "tslib": "2.5.3" @@ -12305,9 +12306,9 @@ "dev": true }, "node_modules/graphiql": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/graphiql/-/graphiql-3.0.10.tgz", - "integrity": "sha512-xgRFCg0mgIyca8keWkmBFA3knh9exDg53SxqFh96ewoMWYLeziqc0xIGFe2L/As8Aw1u5pFZcW913HwX3IXztw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/graphiql/-/graphiql-3.1.0.tgz", + "integrity": "sha512-1l2PecYNvFYYNSYq+4vIJOACXkP60Kod0E0SnKu+2f0Ux/npFNr3TfwJLZs7eKqqSh0KODmorvHi/XBP46Ua7A==", "dependencies": { "@graphiql/react": "^0.20.2", "@graphiql/toolkit": "^0.9.1", @@ -12343,6 +12344,17 @@ "graphql": "^15.5.0 || ^16.0.0" } }, + "node_modules/graphql-ws": { + "version": "5.14.3", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.14.3.tgz", + "integrity": "sha512-F/i2xNIVbaEF2xWggID0X/UZQa2V8kqKDPO8hwmu53bVOcTL7uNkxnexeEgSCVxYBQUTUNEI8+e4LO1FOhKPKQ==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "graphql": ">=0.11 <=16" + } + }, "node_modules/growl": { "version": "1.10.5", "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", @@ -31864,9 +31876,9 @@ "dev": true }, "graphiql": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/graphiql/-/graphiql-3.0.10.tgz", - "integrity": "sha512-xgRFCg0mgIyca8keWkmBFA3knh9exDg53SxqFh96ewoMWYLeziqc0xIGFe2L/As8Aw1u5pFZcW913HwX3IXztw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/graphiql/-/graphiql-3.1.0.tgz", + "integrity": "sha512-1l2PecYNvFYYNSYq+4vIJOACXkP60Kod0E0SnKu+2f0Ux/npFNr3TfwJLZs7eKqqSh0KODmorvHi/XBP46Ua7A==", "requires": { "@graphiql/react": "^0.20.2", "@graphiql/toolkit": "^0.9.1", @@ -31888,6 +31900,12 @@ "vscode-languageserver-types": "^3.17.1" } }, + "graphql-ws": { + "version": "5.14.3", + "resolved": "https://registry.npmjs.org/graphql-ws/-/graphql-ws-5.14.3.tgz", + "integrity": "sha512-F/i2xNIVbaEF2xWggID0X/UZQa2V8kqKDPO8hwmu53bVOcTL7uNkxnexeEgSCVxYBQUTUNEI8+e4LO1FOhKPKQ==", + "requires": {} + }, "growl": { "version": "1.10.5", "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.5.tgz", diff --git a/package.json b/package.json index db3d254..ff470e0 100644 --- a/package.json +++ b/package.json @@ -63,8 +63,9 @@ "@grafana/runtime": "10.0.3", "@grafana/schema": "10.0.3", "@grafana/ui": "10.0.3", - "graphiql": "^3.0.10", + "graphiql": "^3.1.0", "graphql": "^16.8.1", + "graphql-ws": "^5.14.3", "react": "18.2.0", "react-dom": "18.2.0", "tslib": "2.5.3" diff --git a/src/components/QueryEditor.tsx b/src/components/QueryEditor.tsx index 477c93d..647bdec 100644 --- a/src/components/QueryEditor.tsx +++ b/src/components/QueryEditor.tsx @@ -1,4 +1,4 @@ -import React, {ChangeEvent, useMemo} from 'react'; +import React, {ChangeEvent, useEffect, useMemo} from 'react'; import {InlineField, Input} from '@grafana/ui'; import {QueryEditorProps} from '@grafana/data'; import {DataSource} from '../datasource'; @@ -10,15 +10,16 @@ import { PluginContextProvider, SchemaContextProvider, ExecutionContextProvider, - // StorageContextProvider, - // HistoryContextProvider + useEditorContext } from '@graphiql/react'; import {Fetcher} from '@graphiql/toolkit'; import {FetcherOpts, FetcherParams} from "@graphiql/toolkit/src/create-fetcher/types"; -import {getBackendSrv} from "@grafana/runtime"; +import {getBackendSrv, getTemplateSrv} from "@grafana/runtime"; import {firstValueFrom} from 'rxjs'; import 'graphiql/graphiql.css'; +import {ExecutionResult} from "graphql-ws"; +import {AUTO_POPULATED_VARIABLES} from "../variables"; // import '@graphiql/react/dist/style.css'; // import '@graphiql/react/font/roboto.css'; @@ -30,9 +31,17 @@ type Props = QueryEditorProps = { @@ -43,30 +52,38 @@ function createFetcher(url: string, withCredentials: boolean, basicAuth?: string headers['Authorization'] = basicAuth; } const backendSrv = getBackendSrv(); + const templateSrv = getTemplateSrv(); return async (graphQLParams: FetcherParams, opts?: FetcherOpts) => { + const variables = { + ...graphQLParams.variables, // TODO warn user if we are overriding their variables with the autopopulated ones + ...AUTO_POPULATED_VARIABLES, + }; + for (const field in variables) { + const value = variables[field]; + if (typeof value === 'string') { + variables[field] = templateSrv.replace(value); + } + } + const query = { + ...graphQLParams, + variables: variables + }; const observable = backendSrv.fetch({ url, headers, method: "POST", - data: graphQLParams, + data: query, responseType: "json", // TODO consider other options available here }); - // TODO handle error cases here + // awaiting the observable may throw an exception, and that's OK, we can let that propagate up const response = await firstValueFrom(observable); - return response.data; - // const data = await fetch(url || '', { - // method: 'POST', - // headers: headers, - // body: JSON.stringify(graphQLParams), - // credentials: 'same-origin', - // }); - // return data.json().catch(() => data.text()); + return response.data as ExecutionResult; }; } -export function QueryEditor({ query, onChange, onRunQuery, datasource }: Props) { - +export function QueryEditor(props: Props) { + const { query, datasource } = props; const fetcher = useMemo(() => { return createFetcher( @@ -76,60 +93,78 @@ export function QueryEditor({ query, onChange, onRunQuery, datasource }: Props) ); }, [datasource.options.url, datasource.options.withCredentials, datasource.options.basicAuth]); + + return ( + <> + {/*By not providing storage, history contexts, they won't be used*/} + {/**/} + {/* */} + + + + {/*Explorer context needed for documentation*/} + + + + + + + + + ); +} + +function InnerQueryEditor({ query, onChange, onRunQuery, datasource }: Props) { + const editorContext = useEditorContext(); const onOperationNameChange = (event: ChangeEvent) => { - onChange({ ...query, operationName: event.target.value || undefined }); + const newOperationName = event.target.value || undefined; + const queryEditor = editorContext?.queryEditor; + if (queryEditor) { + // We don't use editorContext.setOperationName because that function does not accept null values for some reason + // Note to future me - if you need to look at the source of setOperationName, search everywhere for `'setOperationName'` in the graphiql codebase + // NOTE: I'm not sure if setting this value actually does anything + queryEditor.operationName = newOperationName ?? null; + } + // by updating the active tab values, we are able to switch the "active operation" to whatever the user has just typed out + editorContext?.updateActiveTabValues({operationName: newOperationName}) + onChange({ ...query, operationName: newOperationName }); }; + const currentOperationName = editorContext?.queryEditor?.operationName; + useEffect(() => { + // if currentOperationName is null, that means that the query is unnamed + // currentOperationName should never be undefined unless queryEditor is undefined + if (currentOperationName !== undefined && query.operationName !== currentOperationName) { + // Remember that in our world, we use the string | undefined type for operationName, + // so we're basically converting null to undefined here + onChange({ ...query, operationName: currentOperationName ?? undefined }); + } + }, [onChange, query, currentOperationName]); return ( <>

Query

-
- {/*Query*/} - {/* {*/} - {/* console.log("Edited query");*/} - {/* console.log(value);*/} - {/* onChange({...query, queryText: value});*/} - {/* }}*/} - {/* isHeadersEditorEnabled={false}*/} - {/* showPersistHeadersSettings={false}*/} - {/* storage={DummyStorage}*/} - {/* shouldPersistHeaders={false}*/} - {/* plugins={}*/} - {/*/>*/} - - {/*By not providing storage, history contexts, they won't be used*/} - {/**/} - {/* */} - - - - {/*Explorer context needed for documentation*/} - - { - console.log("Edited query"); - console.log(value); - onChange({...query, queryText: value}); - }} - /> - - - - - - +
+ { + onChange({...query, queryText: value}); + }} + onEditVariables={(variablesJsonString) => { + // TODO + }} + />
); + } diff --git a/src/types.ts b/src/types.ts index 88d1b8e..b3f60c0 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,6 +3,7 @@ import { DataQuery } from '@grafana/schema'; interface WildGraphQLCommonQuery extends DataQuery { queryText: string; + /** The operation name if explicitly set. Note that an empty string should be treated the same way as an undefined value, although storing an undefined value is preferred.*/ operationName?: string; } @@ -12,7 +13,19 @@ interface WildGraphQLCommonQuery extends DataQuery { export interface WildGraphQLMainQuery extends WildGraphQLCommonQuery { } + export const DEFAULT_QUERY: Partial = { + queryText: `query BatteryVoltage($from: Long!, $to: Long!) { + queryStatus(sourceId: "default", from: $from, to: $to) { + batteryVoltage { + dateMillis + packet { + batteryVoltage + } + } + } +} +`, }; /** diff --git a/src/variables.ts b/src/variables.ts new file mode 100644 index 0000000..b074633 --- /dev/null +++ b/src/variables.ts @@ -0,0 +1,21 @@ +import {getTemplateSrv} from "@grafana/runtime"; + +/** + * This represents variables that are automatically populated. + * The keys are the variable names, and the values should be interpolated by {@link getTemplateSrv}. + * + * The values should only be used when making a query directly from the frontend (without going through the Go backend). + * + * This can be used as a reference for which values the backend should replace. + * + * NOTE: Do not add variables to this that the backend cannot supply. + * User provided variables should be interpolated on the frontend when possible, + * but we want to reduce the dependency on the frontend here specifically because the backend cannot use {@link getTemplateSrv}. + * Remember THE VALUES HERE ARE NOT USED BY THE BACKEND AND ARE ONLY USED FOR DEBUGGING QUERIES IN THE FRONTEND BY THE RUN BUTTON. + */ +export const AUTO_POPULATED_VARIABLES: Record = { + "from": "$__from", + "to": "$__to", + "interval_ms": "$__interval_ms", +}; +