grafana / scenes

Build Grafana dashboards directly in your Grafana app plugins.
https://grafana.com/developers/scenes
Apache License 2.0
141 stars 21 forks source link

grafana scenes app with backend how to implement StreamHandler #897

Closed suikast42 closed 2 months ago

suikast42 commented 2 months ago

I have scaffolded a demo scenes app with the command

npx @grafana/create-plugin@latest

and choose the option

App with Scenes (create dynamic dashboards in app pages)

The demo works so far so good. I can add further rest endpoints and call it from the frontend with

  const { error, loading, value } = useAsync(() => {
    const backendSrv = getBackendSrv();

    return Promise.all([
      backendSrv.get(`api/plugins/test-test-app/resources/ping`),
      backendSrv.get(`api/plugins/test-test-app/health`),
      backendSrv.post(`api/plugins/test-test-app/resources/echo`, '{"message":"echoooo"}'),
    ]);
  });

And now I am trying to use the streaming api. For that I implement the backend.StreamHandler

// region Stream handling
func (d *App) SubscribeStream(_ context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) {
    return &backend.SubscribeStreamResponse{
        Status: backend.SubscribeStreamStatusOK,
    }, nil
}

// PublishStream just returns permission denied in this case, since in this example we don't want the user to send stream data.
// Permissions verifications could be done here. Check backend.StreamHandler docs for more details.
func (d *App) PublishStream(context.Context, *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) {
    return &backend.PublishStreamResponse{
        Status: backend.PublishStreamStatusPermissionDenied,
    }, nil
}

func (d *App) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error {
    log.DefaultLogger.Info("WS request for path %s. Data ", req.Path, req.Data)
    q := Query{}
    err := json.Unmarshal(req.Data, &q)
    if err != nil {
        log.DefaultLogger.Error(err.Error())
        q = Query{
            TickInterval: 1,
            UpperLimit:   100,
            LowerLimit:   1,
        }
    }
    s := rand.NewSource(time.Now().UnixNano())
    r := rand.New(s)

    ticker := time.NewTicker(q.TickInterval * time.Millisecond)
    defer ticker.Stop()

    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        case <-ticker.C:
            // we generate a random value using the intervals provided by the frontend
            randomValue := r.Float64()*(q.UpperLimit-q.LowerLimit) + q.LowerLimit

            err := sender.SendFrame(
                data.NewFrame(
                    "response",
                    data.NewField("time", nil, []time.Time{time.Now()}),
                    data.NewField("value", nil, []float64{randomValue})),
                data.IncludeAll,
            )

            if err != nil {
                log.DefaultLogger.Error("Failed send frame", "error", err)
            }
        }
    }
}

// endregion
  });

And try to call over

  // How can I  trigger the connection to func (d *App) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) 
// Without implementing custom datasource here ?
  const backendSrv = getGrafanaLiveSrv().getStream({
    namespace: 'ws',
    path: 'test-test-app',
    scope: LiveChannelScope.Stream,
  });

All example that I found implements either a datasource or a scenes app with simple rest backend. But I can't find any example that implements a secenes app without a separate datasource.

Is there a standard way to comine both ?

suikast42 commented 2 months ago

Ok I solve myself with nested plugin.

Hope that's the recommed way.

For folks that want look in to this:

See: https://github.com/suikast42/grafana_apps_test/tree/M2