Skip to content

Commit

Permalink
feat: select resources by name in query builder
Browse files Browse the repository at this point in the history
  • Loading branch information
apricote committed Dec 19, 2023
1 parent 8a435ec commit 84b8857
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 17 deletions.
81 changes: 79 additions & 2 deletions pkg/plugin/datasource.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/build"
"github.com/prometheus/client_golang/prometheus"
"math"
"net/http"
"strconv"
"time"

Expand Down Expand Up @@ -61,6 +62,7 @@ type QueryModel struct {
// interfaces - only those which are required for a particular task.
var (
_ backend.QueryDataHandler = (*Datasource)(nil)
_ backend.CallResourceHandler = (*Datasource)(nil)
_ backend.CheckHealthHandler = (*Datasource)(nil)
_ instancemgmt.InstanceDisposer = (*Datasource)(nil)
)
Expand Down Expand Up @@ -96,9 +98,11 @@ func NewDatasource(_ context.Context, settings backend.DataSourceInstanceSetting
clientOpts...,
)

return &Datasource{
d := &Datasource{
client: client,
}, nil
}

return d, nil
}

// Datasource is an example datasource which can respond to data queries, reports
Expand Down Expand Up @@ -402,3 +406,76 @@ func metricTypeToLoadBalancerMetricType(metricsType MetricsType) hcloud.LoadBala
return hcloud.LoadBalancerMetricOpenConnections
}
}

func (d *Datasource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
ctxLogger := logger.FromContext(ctx)

var returnData any
var err error

switch req.Path {
case "/servers":
returnData, err = d.getServers(ctx)
case "/load-balancers":
returnData, err = d.getLoadBalancers(ctx)
}

if err != nil {
ctxLogger.Warn("failed to respond to resource call", "path", req.Path, "error", err)
return sender.Send(&backend.CallResourceResponse{
Status: http.StatusInternalServerError,
})
}

body, err := json.Marshal(returnData)
if err != nil {
ctxLogger.Warn("failed to encode json body in resource call", "path", req.Path, "error", err)
return sender.Send(&backend.CallResourceResponse{
Status: http.StatusInternalServerError,
})
}

return sender.Send(&backend.CallResourceResponse{
Status: http.StatusOK,
Body: body,
})
}

type SelectableValue struct {
Value int64 `json:"value"`
Label string `json:"label"`
}

func (d *Datasource) getServers(ctx context.Context) ([]SelectableValue, error) {
servers, err := d.client.Server.All(ctx)
if err != nil {
return nil, err
}

selectableValues := make([]SelectableValue, 0, len(servers))
for _, server := range servers {
selectableValues = append(selectableValues, SelectableValue{
Value: server.ID,
Label: server.Name,
})
}

return selectableValues, nil
}

func (d *Datasource) getLoadBalancers(ctx context.Context) ([]SelectableValue, error) {
loadBalancers, err := d.client.LoadBalancer.All(ctx)
if err != nil {
return nil, err
}

selectableValues := make([]SelectableValue, 0, len(loadBalancers))
for _, loadBalancer := range loadBalancers {
selectableValues = append(selectableValues, SelectableValue{
Value: loadBalancer.ID,
Label: loadBalancer.Name,
})
}

return selectableValues, nil
}
48 changes: 35 additions & 13 deletions src/components/QueryEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React from 'react';
import { InlineField, Select } from '@grafana/ui';
import React, { useState } from 'react';
import { AsyncMultiSelect, InlineField, InlineFieldRow, Select } from '@grafana/ui';
import { QueryEditorProps, SelectableValue } from '@grafana/data';
import { DataSource } from '../datasource';
import { DataSourceOptions, LoadBalancerMetricsTypes, Query, ServerMetricsTypes } from '../types';

type Props = QueryEditorProps<DataSource, Query, DataSourceOptions>;

export function QueryEditor({ query, onChange, onRunQuery }: Props) {
export function QueryEditor({ query, onChange, onRunQuery, datasource }: Props) {
const onResourceTypeChange = (event: SelectableValue<Query['resourceType']>) => {
const resourceType = event.value!;
let metricsType = query.metricsType;
Expand All @@ -25,7 +25,8 @@ export function QueryEditor({ query, onChange, onRunQuery }: Props) {
}
}
}
onChange({ ...query, resourceType, metricsType });
onChange({ ...query, resourceType, metricsType, resourceIDs: [] });
setFormResourceIDs([]);
onRunQuery();
};

Expand All @@ -39,12 +40,19 @@ export function QueryEditor({ query, onChange, onRunQuery }: Props) {
onRunQuery();
};

const [formResourceIDs, setFormResourceIDs] = useState<Array<SelectableValue<number>>>([]);
const onResourceNameOrIDsChange = (newValues: Array<SelectableValue<number>>) => {
onChange({ ...query, resourceIDs: newValues.map((value) => value.value!) });
onRunQuery();
setFormResourceIDs(newValues);
};

const availableMetricTypes = query.resourceType === 'server' ? ServerMetricsTypes : LoadBalancerMetricsTypes;

const { queryType, resourceType, metricsType } = query;
const { queryType, resourceType, metricsType, resourceIDs } = query;

return (
<div className="gf-form">
<InlineFieldRow>
<InlineField label="Query Type">
<Select
options={[
Expand Down Expand Up @@ -74,15 +82,29 @@ export function QueryEditor({ query, onChange, onRunQuery }: Props) {
onChange={onMetricsTypeChange}
></Select>
</InlineField>
<InlineField label="Resources" tooltip="ID or Names">
<Select
options={availableMetricTypes.map((type) => ({ label: type, value: type }))}
value={metricsType}
onChange={onMetricsTypeChange}
></Select>

<InlineField label={resourceType === 'server' ? 'Servers' : 'Load Balancers'}>
<AsyncMultiSelect
key={resourceType} // Force reloading options when the key changes
loadOptions={loadResources(datasource, resourceType)}
value={formResourceIDs}
onChange={onResourceNameOrIDsChange}
defaultOptions
></AsyncMultiSelect>
</InlineField>
</>
)}
</div>
</InlineFieldRow>
);
}

const loadResources = (datasource: DataSource, resourceType: Query['resourceType']) => async (_: string) => {
switch (resourceType) {
case 'server': {
return datasource.getServers();
}
case 'load-balancer': {
return datasource.getLoadBalancers();
}
}
};
10 changes: 9 additions & 1 deletion src/datasource.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DataSourceInstanceSettings, CoreApp } from '@grafana/data';
import { DataSourceInstanceSettings, CoreApp, SelectableValue } from '@grafana/data';
import { DataSourceWithBackend } from '@grafana/runtime';

import { Query, DataSourceOptions, DEFAULT_QUERY } from './types';
Expand All @@ -14,4 +14,12 @@ export class DataSource extends DataSourceWithBackend<Query, DataSourceOptions>
getDefaultQuery(_: CoreApp): Partial<Query> {
return DEFAULT_QUERY;
}

async getServers(): Promise<Array<SelectableValue<number>>> {
return this.getResource('/servers');
}

async getLoadBalancers(): Promise<Array<SelectableValue<number>>> {
return this.getResource('/load-balancers');
}
}
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export interface Query extends DataQuery {
resourceType: 'server' | 'load-balancer';
metricsType: (typeof ServerMetricsTypes)[number] | (typeof LoadBalancerMetricsTypes)[number];

resourceNameOrIDs: string[];
resourceIDs: number[];
}

export const DEFAULT_QUERY: Partial<Query> = {
Expand Down

0 comments on commit 84b8857

Please sign in to comment.