Skip to content

Commit

Permalink
Frontend works really well now
Browse files Browse the repository at this point in the history
  • Loading branch information
retrodaredevil committed Jan 7, 2024
1 parent 05dcf00 commit a368264
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 72 deletions.
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
32 changes: 25 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
164 changes: 100 additions & 64 deletions src/components/QueryEditor.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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';
Expand All @@ -30,9 +31,17 @@ type Props = QueryEditorProps<DataSource, WildGraphQLMainQuery, WildGraphQLDataS

/**
* This fetcher is designed to be used only for fetching the schema of a GraphQL endpoint.
* This uses [getBackendSrv] to use Grafana's default backend HTTP proxy.
* This uses {@link getBackendSrv} to use Grafana's default backend HTTP proxy.
* This means that we make requests to the GraphQL endpoint in two different ways, this being the less common and less robust way.
* This is less robust because DataSourceHttpSettings defines many different options, and we don't actually respect all of them here.
*
* This fetcher also automatically performs variable templating using {@link getTemplateSrv}.
* This templating is only applied to the variables themselves, not the queryText.
* This is useful for when pressing the run button on the query editor itself, which (just like the schema)
* is not sent through the more robust backend logic.
* This is consistent with how the query should be altered on the frontend before sending it to the backend.
* One key difference here is that it is expected that all variables populated automatically by the backend
* are also automatically populated by this method, using
*/
function createFetcher(url: string, withCredentials: boolean, basicAuth?: string): Fetcher {
const headers: Record<string, any> = {
Expand All @@ -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(
Expand All @@ -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*/}
{/*<StorageContextProvider storage={DummyStorage}>*/}
{/* <HistoryContextProvider maxHistoryLength={0}>*/}
<EditorContextProvider
defaultQuery={query.queryText}
// we don't need to pass onEditOperationName here because we have a callback that handles it ourselves
>
<SchemaContextProvider fetcher={fetcher}>
<ExecutionContextProvider
fetcher={fetcher}
// NOTE: We don't pass the operationName here because when the user presses the run button,
// we want them to always have to choose which operation they want
>
<ExplorerContextProvider> {/*Explorer context needed for documentation*/}
<PluginContextProvider>
<InnerQueryEditor
{...props}
/>
</PluginContextProvider>
</ExplorerContextProvider>
</ExecutionContextProvider>
</SchemaContextProvider>
</EditorContextProvider>
</>
);
}

function InnerQueryEditor({ query, onChange, onRunQuery, datasource }: Props) {
const editorContext = useEditorContext();
const onOperationNameChange = (event: ChangeEvent<HTMLInputElement>) => {
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 (
<>
<h3 className="page-heading">Query</h3>
<div className="gf-form-group">
<div className="gf-form">
{/*<InlineFormLabel width={13}>Query</InlineFormLabel>*/}
{/*<GraphiQL*/}
{/* fetcher={fetcher}*/}
{/* defaultQuery={query.queryText}*/}
{/* onEditQuery={(value) => {*/}
{/* 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*/}
{/*<StorageContextProvider storage={DummyStorage}>*/}
{/* <HistoryContextProvider maxHistoryLength={0}>*/}
<EditorContextProvider
defaultQuery={query.queryText}
>
<SchemaContextProvider fetcher={fetcher}>
<ExecutionContextProvider
fetcher={fetcher}
// TODO consider passing operationName here
>
<ExplorerContextProvider> {/*Explorer context needed for documentation*/}
<PluginContextProvider>
<GraphiQLInterface
showPersistHeadersSettings={false}
// TODO add disableTabs={true} when release supports https://github.com/graphql/graphiql/pull/3408
isHeadersEditorEnabled={false}
onEditQuery={(value) => {
console.log("Edited query");
console.log(value);
onChange({...query, queryText: value});
}}
/>
</PluginContextProvider>
</ExplorerContextProvider>
</ExecutionContextProvider>
</SchemaContextProvider>
</EditorContextProvider>

<div className="gf-form" style={{height: "450px"}}>
<GraphiQLInterface
showPersistHeadersSettings={false}
disableTabs={true}
isHeadersEditorEnabled={false} // TODO consider enabling customizable headers later
onEditQuery={(value) => {
onChange({...query, queryText: value});
}}
onEditVariables={(variablesJsonString) => {
// TODO
}}
/>
</div>
<div className="gf-form-inline">
<InlineField label="Operation Name" labelWidth={32}
Expand All @@ -140,4 +175,5 @@ export function QueryEditor({ query, onChange, onRunQuery, datasource }: Props)
</div>
</>
);

}
13 changes: 13 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -12,7 +13,19 @@ interface WildGraphQLCommonQuery extends DataQuery {
export interface WildGraphQLMainQuery extends WildGraphQLCommonQuery {
}


export const DEFAULT_QUERY: Partial<WildGraphQLMainQuery> = {
queryText: `query BatteryVoltage($from: Long!, $to: Long!) {
queryStatus(sourceId: "default", from: $from, to: $to) {
batteryVoltage {
dateMillis
packet {
batteryVoltage
}
}
}
}
`,
};

/**
Expand Down
21 changes: 21 additions & 0 deletions src/variables.ts
Original file line number Diff line number Diff line change
@@ -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<string, any> = {
"from": "$__from",
"to": "$__to",
"interval_ms": "$__interval_ms",
};

0 comments on commit a368264

Please sign in to comment.