Live: Remove (alpha) ability to configure live pipelines (#65138)

pull/65229/head
Ryan McKinley 2 years ago committed by GitHub
parent b2fb7a162a
commit f96637b5fc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 30
      .betterer.results
  2. 1
      docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
  3. 1
      packages/grafana-data/src/types/featureToggles.gen.ts
  4. 15
      pkg/api/api.go
  5. 6
      pkg/services/featuremgmt/registry.go
  6. 1
      pkg/services/featuremgmt/toggles_gen.csv
  7. 4
      pkg/services/featuremgmt/toggles_gen.go
  8. 1
      pkg/services/featuremgmt/toggles_gen_test.go
  9. 48
      pkg/services/live/live.go
  10. 25
      pkg/services/navtree/navtreeimpl/navtree.go
  11. 10
      pkg/tsdb/testdatasource/stream_handler.go
  12. 8
      public/app/core/components/NavBar/navBarItem-translations.ts
  13. 132
      public/app/features/live/pages/AddNewRule.tsx
  14. 51
      public/app/features/live/pages/CloudAdminPage.tsx
  15. 22
      public/app/features/live/pages/FeatureTogglePage.tsx
  16. 14
      public/app/features/live/pages/LiveStatusPage.tsx
  17. 67
      public/app/features/live/pages/PipelineAdminPage.tsx
  18. 142
      public/app/features/live/pages/PipelineTable.tsx
  19. 128
      public/app/features/live/pages/RuleModal.tsx
  20. 51
      public/app/features/live/pages/RuleSettingsArray.tsx
  21. 48
      public/app/features/live/pages/RuleSettingsEditor.tsx
  22. 78
      public/app/features/live/pages/RuleTest.tsx
  23. 40
      public/app/features/live/pages/routes.ts
  24. 61
      public/app/features/live/pages/types.ts
  25. 31
      public/app/features/live/pages/utils.ts
  26. 132
      public/app/features/live/pipeline/models.gen.ts
  27. 2
      public/app/routes/routes.tsx
  28. 12
      public/locales/de-DE/grafana.json
  29. 12
      public/locales/en-US/grafana.json
  30. 12
      public/locales/es-ES/grafana.json
  31. 12
      public/locales/fr-FR/grafana.json
  32. 12
      public/locales/pseudo-LOCALE/grafana.json
  33. 12
      public/locales/zh-Hans/grafana.json

@ -3469,36 +3469,6 @@ exports[`better eslint`] = {
"public/app/features/live/index.ts:5381": [ "public/app/features/live/index.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"] [0, 0, 0, "Do not use any type assertions.", "0"]
], ],
"public/app/features/live/pages/AddNewRule.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/live/pages/PipelineAdminPage.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/live/pages/PipelineTable.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/live/pages/RuleModal.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
],
"public/app/features/live/pages/RuleTest.tsx:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"]
],
"public/app/features/live/pages/types.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
],
"public/app/features/live/pages/utils.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],
"public/app/features/logs/components/LogRowContextProvider.tsx:5381": [ "public/app/features/logs/components/LogRowContextProvider.tsx:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"], [0, 0, 0, "Do not use any type assertions.", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"], [0, 0, 0, "Do not use any type assertions.", "1"],

@ -56,7 +56,6 @@ Alpha features might be changed or removed without prior notice.
| ---------------------------------- | --------------------------------------------------------------------------------------------------------- | | ---------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `alertingBigTransactions` | Use big transactions for alerting database writes | | `alertingBigTransactions` | Use big transactions for alerting database writes |
| `dashboardPreviews` | Create and show thumbnails for dashboard search results | | `dashboardPreviews` | Create and show thumbnails for dashboard search results |
| `live-pipeline` | Enable a generic live processing pipeline |
| `live-service-web-worker` | This will use a webworker thread to processes events rather than the main thread | | `live-service-web-worker` | This will use a webworker thread to processes events rather than the main thread |
| `queryOverLive` | Use Grafana Live WebSocket to execute backend queries | | `queryOverLive` | Use Grafana Live WebSocket to execute backend queries |
| `publicDashboards` | Enables public access to dashboards | | `publicDashboards` | Enables public access to dashboards |

@ -23,7 +23,6 @@ export interface FeatureToggles {
disableEnvelopeEncryption?: boolean; disableEnvelopeEncryption?: boolean;
database_metrics?: boolean; database_metrics?: boolean;
dashboardPreviews?: boolean; dashboardPreviews?: boolean;
['live-pipeline']?: boolean;
['live-service-web-worker']?: boolean; ['live-service-web-worker']?: boolean;
queryOverLive?: boolean; queryOverLive?: boolean;
panelTitleSearch?: boolean; panelTitleSearch?: boolean;

@ -591,21 +591,6 @@ func (hs *HTTPServer) registerRoutes() {
// Some channels may have info // Some channels may have info
liveRoute.Get("/info/*", routing.Wrap(hs.Live.HandleInfoHTTP)) liveRoute.Get("/info/*", routing.Wrap(hs.Live.HandleInfoHTTP))
if hs.Features.IsEnabled(featuremgmt.FlagLivePipeline) {
// POST Live data to be processed according to channel rules.
liveRoute.Post("/pipeline/push/*", hs.LivePushGateway.HandlePipelinePush)
liveRoute.Post("/pipeline-convert-test", routing.Wrap(hs.Live.HandlePipelineConvertTestHTTP), reqOrgAdmin)
liveRoute.Get("/pipeline-entities", routing.Wrap(hs.Live.HandlePipelineEntitiesListHTTP), reqOrgAdmin)
liveRoute.Get("/channel-rules", routing.Wrap(hs.Live.HandleChannelRulesListHTTP), reqOrgAdmin)
liveRoute.Post("/channel-rules", routing.Wrap(hs.Live.HandleChannelRulesPostHTTP), reqOrgAdmin)
liveRoute.Put("/channel-rules", routing.Wrap(hs.Live.HandleChannelRulesPutHTTP), reqOrgAdmin)
liveRoute.Delete("/channel-rules", routing.Wrap(hs.Live.HandleChannelRulesDeleteHTTP), reqOrgAdmin)
liveRoute.Get("/write-configs", routing.Wrap(hs.Live.HandleWriteConfigsListHTTP), reqOrgAdmin)
liveRoute.Post("/write-configs", routing.Wrap(hs.Live.HandleWriteConfigsPostHTTP), reqOrgAdmin)
liveRoute.Put("/write-configs", routing.Wrap(hs.Live.HandleWriteConfigsPutHTTP), reqOrgAdmin)
liveRoute.Delete("/write-configs", routing.Wrap(hs.Live.HandleWriteConfigsDeleteHTTP), reqOrgAdmin)
}
}) })
// short urls // short urls

@ -39,12 +39,6 @@ var (
State: FeatureStateAlpha, State: FeatureStateAlpha,
Owner: grafanaAppPlatformSquad, Owner: grafanaAppPlatformSquad,
}, },
{
Name: "live-pipeline",
Description: "Enable a generic live processing pipeline",
State: FeatureStateAlpha,
Owner: grafanaAppPlatformSquad,
},
{ {
Name: "live-service-web-worker", Name: "live-service-web-worker",
Description: "This will use a webworker thread to processes events rather than the main thread", Description: "This will use a webworker thread to processes events rather than the main thread",

@ -4,7 +4,6 @@ trimDefaults,beta,@grafana/grafana-as-code,false,false,false,false
disableEnvelopeEncryption,stable,@grafana/grafana-as-code,false,false,false,false disableEnvelopeEncryption,stable,@grafana/grafana-as-code,false,false,false,false
database_metrics,stable,@grafana/hosted-grafana-team,false,false,false,false database_metrics,stable,@grafana/hosted-grafana-team,false,false,false,false
dashboardPreviews,alpha,@grafana/grafana-app-platform-squad,false,false,false,false dashboardPreviews,alpha,@grafana/grafana-app-platform-squad,false,false,false,false
live-pipeline,alpha,@grafana/grafana-app-platform-squad,false,false,false,false
live-service-web-worker,alpha,@grafana/grafana-app-platform-squad,false,false,false,true live-service-web-worker,alpha,@grafana/grafana-app-platform-squad,false,false,false,true
queryOverLive,alpha,@grafana/grafana-app-platform-squad,false,false,false,true queryOverLive,alpha,@grafana/grafana-app-platform-squad,false,false,false,true
panelTitleSearch,beta,@grafana/grafana-app-platform-squad,false,false,false,false panelTitleSearch,beta,@grafana/grafana-app-platform-squad,false,false,false,false

1 Name State Owner requiresDevMode RequiresLicense RequiresRestart FrontendOnly
4 disableEnvelopeEncryption stable @grafana/grafana-as-code false false false false
5 database_metrics stable @grafana/hosted-grafana-team false false false false
6 dashboardPreviews alpha @grafana/grafana-app-platform-squad false false false false
live-pipeline alpha @grafana/grafana-app-platform-squad false false false false
7 live-service-web-worker alpha @grafana/grafana-app-platform-squad false false false true
8 queryOverLive alpha @grafana/grafana-app-platform-squad false false false true
9 panelTitleSearch beta @grafana/grafana-app-platform-squad false false false false

@ -27,10 +27,6 @@ const (
// Create and show thumbnails for dashboard search results // Create and show thumbnails for dashboard search results
FlagDashboardPreviews = "dashboardPreviews" FlagDashboardPreviews = "dashboardPreviews"
// FlagLivePipeline
// Enable a generic live processing pipeline
FlagLivePipeline = "live-pipeline"
// FlagLiveServiceWebWorker // FlagLiveServiceWebWorker
// This will use a webworker thread to processes events rather than the main thread // This will use a webworker thread to processes events rather than the main thread
FlagLiveServiceWebWorker = "live-service-web-worker" FlagLiveServiceWebWorker = "live-service-web-worker"

@ -24,7 +24,6 @@ func TestFeatureToggleFiles(t *testing.T) {
"httpclientprovider_azure_auth": true, "httpclientprovider_azure_auth": true,
"service-accounts": true, "service-accounts": true,
"database_metrics": true, "database_metrics": true,
"live-pipeline": true,
"live-service-web-worker": true, "live-service-web-worker": true,
"k8s": true, // Camel case does not like this one "k8s": true, // Camel case does not like this one
} }

@ -8,7 +8,6 @@ import (
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
"os"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
@ -182,53 +181,6 @@ func ProvideService(plugCtxProvider *plugincontext.Provider, cfg *setting.Cfg, r
} }
g.ManagedStreamRunner = managedStreamRunner g.ManagedStreamRunner = managedStreamRunner
if g.Features.IsEnabled(featuremgmt.FlagLivePipeline) {
var builder pipeline.RuleBuilder
if os.Getenv("GF_LIVE_DEV_BUILDER") != "" {
builder = &pipeline.DevRuleBuilder{
Node: node,
ManagedStream: g.ManagedStreamRunner,
FrameStorage: pipeline.NewFrameStorage(),
ChannelHandlerGetter: g,
}
} else {
storage := &pipeline.FileStorage{
DataPath: cfg.DataPath,
SecretsService: g.SecretsService,
}
g.pipelineStorage = storage
builder = &pipeline.StorageRuleBuilder{
Node: node,
ManagedStream: g.ManagedStreamRunner,
FrameStorage: pipeline.NewFrameStorage(),
Storage: storage,
ChannelHandlerGetter: g,
SecretsService: g.SecretsService,
}
}
channelRuleGetter := pipeline.NewCacheSegmentedTree(builder)
// Pre-build/validate channel rules for all organizations on start.
// This can be unreasonable to have in production scenario with many
// organizations.
orgQuery := &org.SearchOrgsQuery{}
result, err := orgService.Search(context.Background(), orgQuery)
if err != nil {
return nil, fmt.Errorf("can't get org list: %w", err)
}
for _, org := range result {
_, _, err := channelRuleGetter.Get(org.ID, "")
if err != nil {
return nil, fmt.Errorf("error building channel rules for org %d: %w", org.ID, err)
}
}
g.Pipeline, err = pipeline.New(channelRuleGetter)
if err != nil {
return nil, err
}
}
g.contextGetter = liveplugin.NewContextGetter(g.PluginContextProvider, g.DataSourceCache) g.contextGetter = liveplugin.NewContextGetter(g.PluginContextProvider, g.DataSourceCache)
pipelinedChannelLocalPublisher := liveplugin.NewChannelLocalPublisher(node, g.Pipeline) pipelinedChannelLocalPublisher := liveplugin.NewChannelLocalPublisher(node, g.Pipeline)

@ -149,31 +149,6 @@ func (s *ServiceImpl) GetNavTree(c *contextmodel.ReqContext, hasEditPerm bool, p
} }
} }
if s.features.IsEnabled(featuremgmt.FlagLivePipeline) {
liveNavLinks := []*navtree.NavLink{}
liveNavLinks = append(liveNavLinks, &navtree.NavLink{
Text: "Status", Id: "live-status", Url: s.cfg.AppSubURL + "/live", Icon: "exchange-alt",
})
liveNavLinks = append(liveNavLinks, &navtree.NavLink{
Text: "Pipeline", Id: "live-pipeline", Url: s.cfg.AppSubURL + "/live/pipeline", Icon: "arrow-to-right",
})
liveNavLinks = append(liveNavLinks, &navtree.NavLink{
Text: "Cloud", Id: "live-cloud", Url: s.cfg.AppSubURL + "/live/cloud", Icon: "cloud-upload",
})
treeRoot.AddSection(&navtree.NavLink{
Id: "live",
Text: "Live",
SubTitle: "Event streaming",
Icon: "exchange-alt",
Url: s.cfg.AppSubURL + "/live",
Children: liveNavLinks,
Section: navtree.NavSectionConfig,
HideFromTabs: true,
})
}
orgAdminNode, err := s.getOrgAdminNode(c) orgAdminNode, err := s.getOrgAdminNode(c)
if orgAdminNode != nil { if orgAdminNode != nil {

@ -10,7 +10,6 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/services/featuremgmt"
) )
var random20HzStreamRegex = regexp.MustCompile(`random-20Hz-stream(-\d+)?`) var random20HzStreamRegex = regexp.MustCompile(`random-20Hz-stream(-\d+)?`)
@ -34,11 +33,6 @@ func (s *Service) SubscribeStream(ctx context.Context, req *backend.SubscribeStr
} }
} }
if s.features.IsEnabled(featuremgmt.FlagLivePipeline) {
// While developing Live pipeline avoid sending initial data.
initialData = nil
}
return &backend.SubscribeStreamResponse{ return &backend.SubscribeStreamResponse{
Status: backend.SubscribeStreamStatusOK, Status: backend.SubscribeStreamStatusOK,
InitialData: initialData, InitialData: initialData,
@ -121,10 +115,6 @@ func (s *Service) runTestStream(ctx context.Context, path string, conf testStrea
} }
mode := data.IncludeDataOnly mode := data.IncludeDataOnly
if s.features.IsEnabled(featuremgmt.FlagLivePipeline) {
mode = data.IncludeAll
}
delta := rand.Float64() - 0.5 delta := rand.Float64() - 0.5
walker += delta walker += delta

@ -109,14 +109,6 @@ export function getNavTitle(navId: string | undefined) {
return t('nav.storage.title', 'Storage'); return t('nav.storage.title', 'Storage');
case 'upgrading': case 'upgrading':
return t('nav.upgrading.title', 'Stats and license'); return t('nav.upgrading.title', 'Stats and license');
case 'live':
return t('nav.live.title', 'Event streaming');
case 'live-status':
return t('nav.live-status.title', 'Status');
case 'live-pipeline':
return t('nav.live-pipeline.title', 'Pipeline');
case 'live-cloud':
return t('nav.live-cloud.title', 'Cloud');
case 'monitoring': case 'monitoring':
return t('nav.monitoring.title', 'Monitoring'); return t('nav.monitoring.title', 'Monitoring');
case 'apps': case 'apps':

@ -1,132 +0,0 @@
import React, { useState } from 'react';
import { DataSourceRef, LiveChannelScope, SelectableValue } from '@grafana/data';
import { DataSourcePicker, getBackendSrv } from '@grafana/runtime';
import { Input, Field, Button, ValuePicker, HorizontalGroup } from '@grafana/ui';
import { useAppNotification } from 'app/core/copy/appNotification';
import { Rule } from './types';
interface Props {
onRuleAdded: (rule: Rule) => void;
}
type PatternType = 'ds' | 'any';
const patternTypes: Array<SelectableValue<PatternType>> = [
{
label: 'Data source',
description: 'Configure a channel scoped to a data source instance',
value: 'ds',
},
{
label: 'Any',
description: 'Enter an arbitray channel pattern',
value: 'any',
},
];
export function AddNewRule({ onRuleAdded }: Props) {
const [patternType, setPatternType] = useState<PatternType>();
const [pattern, setPattern] = useState<string>();
const [patternPrefix, setPatternPrefix] = useState<string>('');
const [datasource, setDatasource] = useState<DataSourceRef>();
const notifyApp = useAppNotification();
const onSubmit = () => {
if (!pattern) {
notifyApp.error('Enter path');
return;
}
if (patternType === 'ds' && !patternPrefix.length) {
notifyApp.error('Select datasource');
return;
}
getBackendSrv()
.post(`api/live/channel-rules`, {
pattern: patternPrefix + pattern,
settings: {
converter: {
type: 'jsonAuto',
},
frameOutputs: [
{
type: 'managedStream',
},
],
},
})
.then((v: any) => {
console.log('ADDED', v);
setPattern(undefined);
setPatternType(undefined);
onRuleAdded(v.rule);
})
.catch((e) => {
notifyApp.error('Error adding rule', e);
e.isHandled = true;
});
};
if (patternType) {
return (
<div>
<HorizontalGroup>
{patternType === 'any' && (
<Field label="Pattern">
<Input
value={pattern ?? ''}
onChange={(e) => setPattern(e.currentTarget.value)}
placeholder="scope/namespace/path"
/>
</Field>
)}
{patternType === 'ds' && (
<>
<Field label="Data source">
<DataSourcePicker
current={datasource}
onChange={(ds) => {
setDatasource(ds);
setPatternPrefix(`${LiveChannelScope.DataSource}/${ds.uid}/`);
}}
/>
</Field>
<Field label="Path">
<Input value={pattern ?? ''} onChange={(e) => setPattern(e.currentTarget.value)} placeholder="path" />
</Field>
</>
)}
<Field label="">
<Button onClick={onSubmit} variant={pattern?.length ? 'primary' : 'secondary'}>
Add
</Button>
</Field>
<Field label="">
<Button variant="secondary" onClick={() => setPatternType(undefined)}>
Cancel
</Button>
</Field>
</HorizontalGroup>
</div>
);
}
return (
<div>
<ValuePicker
label="Add channel rule"
variant="secondary"
size="md"
icon="plus"
menuPlacement="auto"
isFullWidth={false}
options={patternTypes}
onChange={(v) => setPatternType(v.value)}
/>
</div>
);
}

@ -1,51 +0,0 @@
import { css } from '@emotion/css';
import React, { useEffect, useState } from 'react';
import { getBackendSrv } from '@grafana/runtime';
import { Page } from 'app/core/components/Page/Page';
import { useNavModel } from 'app/core/hooks/useNavModel';
import { GrafanaCloudBackend } from './types';
export default function CloudAdminPage() {
const navModel = useNavModel('live-cloud');
const [cloud, setCloud] = useState<GrafanaCloudBackend[]>([]);
const [error, setError] = useState<string>();
useEffect(() => {
getBackendSrv()
.get(`api/live/write-configs`)
.then((data) => {
setCloud(data.writeConfigs);
})
.catch((e) => {
if (e.data) {
setError(JSON.stringify(e.data, null, 2));
}
});
}, []);
return (
<Page navModel={navModel}>
<Page.Contents>
{error && <pre>{error}</pre>}
{!cloud && <>Loading cloud definitions</>}
{cloud &&
cloud.map((v) => {
return (
<div key={v.uid}>
<h2>{v.uid}</h2>
<pre className={styles.row}>{JSON.stringify(v.settings, null, 2)}</pre>
</div>
);
})}
</Page.Contents>
</Page>
);
}
const styles = {
row: css`
cursor: pointer;
`,
};

@ -1,22 +0,0 @@
import React from 'react';
import { Page } from 'app/core/components/Page/Page';
import { useNavModel } from 'app/core/hooks/useNavModel';
export default function FeatureTogglePage() {
const navModel = useNavModel('live-status');
return (
<Page navModel={navModel}>
<Page.Contents>
<h1>Pipeline is not enabled</h1>
To enable pipelines, enable the feature toggle:
<pre>
{`[feature_toggles]
enable = live-pipeline
`}
</pre>
</Page.Contents>
</Page>
);
}

@ -1,14 +0,0 @@
import React from 'react';
import { Page } from 'app/core/components/Page/Page';
import { useNavModel } from 'app/core/hooks/useNavModel';
export default function CloudAdminPage() {
const navModel = useNavModel('live-status');
return (
<Page navModel={navModel}>
<Page.Contents>Live/Live/Live</Page.Contents>
</Page>
);
}

@ -1,67 +0,0 @@
import React, { useEffect, useState, ChangeEvent } from 'react';
import { getBackendSrv } from '@grafana/runtime';
import { Input } from '@grafana/ui';
import { Page } from 'app/core/components/Page/Page';
import { useNavModel } from 'app/core/hooks/useNavModel';
import { AddNewRule } from './AddNewRule';
import { PipelineTable } from './PipelineTable';
import { Rule } from './types';
export default function PipelineAdminPage() {
const [rules, setRules] = useState<Rule[]>([]);
const [defaultRules, setDefaultRules] = useState<any[]>([]);
const [newRule, setNewRule] = useState<Rule>();
const navModel = useNavModel('live-pipeline');
const [error, setError] = useState<string>();
const loadRules = () => {
getBackendSrv()
.get(`api/live/channel-rules`)
.then((data) => {
setRules(data.rules ?? []);
setDefaultRules(data.rules ?? []);
})
.catch((e) => {
if (e.data) {
setError(JSON.stringify(e.data, null, 2));
}
});
};
useEffect(() => {
loadRules();
}, []);
const onSearchQueryChange = (e: ChangeEvent<HTMLInputElement>) => {
if (e.target.value) {
setRules(rules.filter((rule) => rule.pattern.toLowerCase().includes(e.target.value.toLowerCase())));
} else {
setRules(defaultRules);
}
};
return (
<Page navModel={navModel}>
<Page.Contents>
{error && <pre>{error}</pre>}
<div className="page-action-bar">
<div className="gf-form gf-form--grow">
<Input placeholder="Search pattern..." onChange={onSearchQueryChange} />
</div>
</div>
<PipelineTable rules={rules} onRuleChanged={loadRules} selectRule={newRule} />
<AddNewRule
onRuleAdded={(r: Rule) => {
console.log('GOT', r, 'vs', rules[0]);
setNewRule(r);
loadRules();
}}
/>
</Page.Contents>
</Page>
);
}

@ -1,142 +0,0 @@
import { css } from '@emotion/css';
import React, { useEffect, useState } from 'react';
import { getBackendSrv } from '@grafana/runtime';
import { Tag, IconButton } from '@grafana/ui';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { RuleModal } from './RuleModal';
import { Rule, Output, RuleType } from './types';
function renderOutputTags(key: string, output?: Output): React.ReactNode {
if (!output?.type) {
return null;
}
return <Tag key={key} name={output.type} />;
}
interface Props {
rules: Rule[];
onRuleChanged: () => void;
selectRule?: Rule;
}
export const PipelineTable = (props: Props) => {
const { rules } = props;
const [isOpen, setOpen] = useState(false);
const [selectedRule, setSelectedRule] = useState<Rule>();
const [clickColumn, setClickColumn] = useState<RuleType>('converter');
const onRowClick = (rule: Rule, event?: any) => {
if (!rule) {
return;
}
let column = event?.target?.getAttribute('data-column');
if (!column || column === 'pattern') {
column = 'converter';
}
setClickColumn(column);
setSelectedRule(rule);
setOpen(true);
};
// Supports selecting a rule from external config (after add rule)
useEffect(() => {
if (props.selectRule) {
onRowClick(props.selectRule);
}
}, [props.selectRule]);
const onRemoveRule = (pattern: string) => {
getBackendSrv()
.delete(`api/live/channel-rules`, JSON.stringify({ pattern: pattern }))
.catch((e) => console.error(e))
.finally(() => {
props.onRuleChanged();
});
};
const renderPattern = (pattern: string) => {
if (pattern.startsWith('ds/')) {
const idx = pattern.indexOf('/', 4);
if (idx > 3) {
const uid = pattern.substring(3, idx);
const ds = getDatasourceSrv().getInstanceSettings(uid);
if (ds) {
return (
<div>
<Tag name={ds.name} colorIndex={1} /> &nbsp;
<span>{pattern.substring(idx + 1)}</span>
</div>
);
}
}
}
return pattern;
};
return (
<div>
<div className="admin-list-table">
<table className="filter-table filter-table--hover form-inline">
<thead>
<tr>
<th>Channel</th>
<th>Converter</th>
<th>Processor</th>
<th>Output</th>
<th style={{ width: 10 }}>&nbsp;</th>
</tr>
</thead>
<tbody>
{rules.map((rule) => (
<tr key={rule.pattern} onClick={(e) => onRowClick(rule, e)} className={styles.row}>
<td data-pattern={rule.pattern} data-column="pattern">
{renderPattern(rule.pattern)}
</td>
<td data-pattern={rule.pattern} data-column="converter">
{rule.settings?.converter?.type}
</td>
<td data-pattern={rule.pattern} data-column="processor">
{rule.settings?.frameProcessors?.map((processor) => (
<span key={rule.pattern + processor.type}>{processor.type}</span>
))}
</td>
<td data-pattern={rule.pattern} data-column="output">
{rule.settings?.frameOutputs?.map((output) => (
<span key={rule.pattern + output.type}>{renderOutputTags('out', output)}</span>
))}
</td>
<td>
<IconButton
name="trash-alt"
onClick={(e) => {
e.stopPropagation();
onRemoveRule(rule.pattern);
}}
></IconButton>
</td>
</tr>
))}
</tbody>
</table>
</div>
{isOpen && selectedRule && (
<RuleModal
rule={selectedRule}
isOpen={isOpen}
onClose={() => {
setOpen(false);
}}
clickColumn={clickColumn}
/>
)}
</div>
);
};
const styles = {
row: css`
cursor: pointer;
`,
};

@ -1,128 +0,0 @@
import { css } from '@emotion/css';
import React, { useState, useMemo } from 'react';
import { getBackendSrv } from '@grafana/runtime';
import { Modal, TabContent, TabsBar, Tab, Button } from '@grafana/ui';
import { RuleSettingsArray } from './RuleSettingsArray';
import { RuleSettingsEditor } from './RuleSettingsEditor';
import { RuleTest } from './RuleTest';
import { Rule, RuleType, PipeLineEntitiesInfo, RuleSetting } from './types';
import { getPipeLineEntities } from './utils';
interface Props {
rule: Rule;
isOpen: boolean;
onClose: () => void;
clickColumn: RuleType;
}
interface TabInfo {
label: string;
type?: RuleType;
isTest?: boolean;
isConverter?: boolean;
icon?: string;
}
const tabs: TabInfo[] = [
{ label: 'Converter', type: 'converter', isConverter: true },
{ label: 'Processors', type: 'frameProcessors' },
{ label: 'Outputs', type: 'frameOutputs' },
{ label: 'Test', isTest: true, icon: 'flask' },
];
export const RuleModal = (props: Props) => {
const { isOpen, onClose, clickColumn } = props;
const [rule, setRule] = useState<Rule>(props.rule);
const [activeTab, setActiveTab] = useState<TabInfo | undefined>(tabs.find((t) => t.type === clickColumn));
// to show color of Save button
const [hasChange, setChange] = useState<boolean>(false);
const [ruleSetting, setRuleSetting] = useState<any>(activeTab?.type ? rule?.settings?.[activeTab.type] : undefined);
const [entitiesInfo, setEntitiesInfo] = useState<PipeLineEntitiesInfo>();
const onRuleSettingChange = (value: RuleSetting | RuleSetting[]) => {
setChange(true);
if (activeTab?.type) {
setRule({
...rule,
settings: {
...rule.settings,
[activeTab?.type]: value,
},
});
}
setRuleSetting(value);
};
// load pipeline entities info
useMemo(() => {
getPipeLineEntities().then((data) => {
setEntitiesInfo(data);
});
}, []);
const onSave = () => {
getBackendSrv()
.put(`api/live/channel-rules`, rule)
.then(() => {
setChange(false);
onClose();
})
.catch((e) => console.error(e));
};
return (
<Modal isOpen={isOpen} title={rule.pattern} onDismiss={onClose} closeOnEscape>
<TabsBar>
{tabs.map((tab, index) => {
return (
<Tab
key={index}
label={tab.label}
active={tab === activeTab}
icon={tab.icon as any}
onChangeTab={() => {
setActiveTab(tab);
if (tab.type) {
// to notify children of the new rule
setRuleSetting(rule?.settings?.[tab.type]);
}
}}
/>
);
})}
</TabsBar>
<TabContent>
{entitiesInfo && rule && activeTab && (
<>
{activeTab?.isTest && <RuleTest rule={rule} />}
{activeTab.isConverter && (
<RuleSettingsEditor
onChange={onRuleSettingChange}
value={ruleSetting}
ruleType={'converter'}
entitiesInfo={entitiesInfo}
/>
)}
{!activeTab.isConverter && activeTab.type && (
<RuleSettingsArray
onChange={onRuleSettingChange}
value={ruleSetting}
ruleType={activeTab.type}
entitiesInfo={entitiesInfo}
/>
)}
</>
)}
<Button onClick={onSave} className={styles.save} variant={hasChange ? 'primary' : 'secondary'}>
Save
</Button>
</TabContent>
</Modal>
);
};
const styles = {
save: css`
margin-top: 5px;
`,
};

@ -1,51 +0,0 @@
import React, { useState } from 'react';
import { SelectableValue } from '@grafana/data';
import { Select } from '@grafana/ui';
import { RuleSettingsEditor } from './RuleSettingsEditor';
import { RuleType, RuleSetting, PipeLineEntitiesInfo } from './types';
interface Props {
ruleType: RuleType;
onChange: (value: RuleSetting[]) => void;
value: RuleSetting[];
entitiesInfo: PipeLineEntitiesInfo;
}
export const RuleSettingsArray = ({ onChange, value, ruleType, entitiesInfo }: Props) => {
const [index, setIndex] = useState<number>(0);
const arr = value ?? [];
const onRuleChange = (v: RuleSetting) => {
if (!value) {
onChange([v]);
} else {
const copy = [...value];
copy[index] = v;
onChange(copy);
}
};
// create array of value.length + 1
let indexArr: Array<SelectableValue<number>> = [];
for (let i = 0; i <= arr.length; i++) {
indexArr.push({
label: `${ruleType}: ${i}`,
value: i,
});
}
return (
<>
<Select
placeholder="Select an index"
options={indexArr}
value={index}
onChange={(index) => {
// set index to find the correct setting
setIndex(index.value!);
}}
></Select>
<RuleSettingsEditor onChange={onRuleChange} value={arr[index]} ruleType={ruleType} entitiesInfo={entitiesInfo} />
</>
);
};

@ -1,48 +0,0 @@
import React from 'react';
import { CodeEditor, Select } from '@grafana/ui';
import { RuleType, RuleSetting, PipeLineEntitiesInfo } from './types';
interface Props {
ruleType: RuleType;
onChange: (value: RuleSetting) => void;
value: RuleSetting;
entitiesInfo: PipeLineEntitiesInfo;
}
export const RuleSettingsEditor = ({ onChange, value, ruleType, entitiesInfo }: Props) => {
return (
<>
<Select
key={ruleType}
options={entitiesInfo[ruleType]}
placeholder="Select an option"
value={value?.type ?? ''}
onChange={(value) => {
// set the body with example
const type = value.value;
onChange({
type,
[type]: entitiesInfo.getExample(ruleType, type),
});
}}
/>
<CodeEditor
height={'50vh'}
value={value ? JSON.stringify(value[value.type], null, '\t') : ''}
showLineNumbers={true}
readOnly={false}
language="json"
showMiniMap={false}
onBlur={(text: string) => {
const body = JSON.parse(text);
onChange({
type: value.type,
[value.type]: body,
});
}}
/>
</>
);
};

@ -1,78 +0,0 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import { dataFrameFromJSON, getDisplayProcessor } from '@grafana/data';
import { getBackendSrv, config } from '@grafana/runtime';
import { Button, CodeEditor, Table, Field } from '@grafana/ui';
import { ChannelFrame, Rule } from './types';
interface Props {
rule: Rule;
}
export const RuleTest = (props: Props) => {
const [response, setResponse] = useState<ChannelFrame[]>();
const [data, setData] = useState<string>();
const onBlur = (text: string) => {
setData(text);
};
const onClick = () => {
getBackendSrv()
.post(`api/live/pipeline-convert-test`, {
channelRules: [props.rule],
channel: props.rule.pattern,
data: data,
})
.then((data: any) => {
const t = data.channelFrames as any[];
if (t) {
setResponse(
t.map((f) => {
const frame = dataFrameFromJSON(f.frame);
for (const field of frame.fields) {
field.display = getDisplayProcessor({ field, theme: config.theme2 });
}
return { channel: f.channel, frame };
})
);
}
})
.catch((e) => {
setResponse(e);
});
};
return (
<div>
<CodeEditor
height={100}
value=""
showLineNumbers={true}
readOnly={false}
language="json"
showMiniMap={false}
onBlur={onBlur}
/>
<Button onClick={onClick} className={styles.margin}>
Test
</Button>
{response?.length &&
response.map((r) => (
<Field key={r.channel} label={r.channel}>
<Table data={r.frame} width={700} height={Math.min(10 * r.frame.length + 10, 150)} showTypeIcons></Table>
</Field>
))}
</div>
);
};
const styles = {
margin: css`
margin-bottom: 15px;
`,
};

@ -1,40 +0,0 @@
import { SafeDynamicImport } from 'app/core/components/DynamicImports/SafeDynamicImport';
import { config } from 'app/core/config';
import { RouteDescriptor } from 'app/core/navigation/types';
import { isGrafanaAdmin } from 'app/features/plugins/admin/permissions';
const liveRoutes = [
{
path: '/live',
component: SafeDynamicImport(
() => import(/* webpackChunkName: "LiveStatusPage" */ 'app/features/live/pages/LiveStatusPage')
),
},
{
path: '/live/pipeline',
component: SafeDynamicImport(
() => import(/* webpackChunkName: "PipelineAdminPage" */ 'app/features/live/pages/PipelineAdminPage')
),
},
{
path: '/live/cloud',
component: SafeDynamicImport(
() => import(/* webpackChunkName: "CloudAdminPage" */ 'app/features/live/pages/CloudAdminPage')
),
},
];
export function getLiveRoutes(cfg = config): RouteDescriptor[] {
if (!isGrafanaAdmin()) {
return [];
}
if (cfg.featureToggles['live-pipeline']) {
return liveRoutes;
}
return liveRoutes.map((v) => ({
...v,
component: SafeDynamicImport(
() => import(/* webpackChunkName: "FeatureTogglePage" */ 'app/features/live/pages/FeatureTogglePage')
),
}));
}

@ -1,61 +0,0 @@
import { DataFrame, SelectableValue } from '@grafana/data';
export interface Converter extends RuleSetting {
[t: string]: any;
}
export interface Processor extends RuleSetting {
[t: string]: any;
}
export interface Output extends RuleSetting {
[t: string]: any;
}
export interface RuleSetting<T = any> {
type: string;
[key: string]: any;
}
export interface RuleSettings {
converter?: Converter;
frameProcessors?: Processor[];
frameOutputs?: Output[];
}
export interface Rule {
pattern: string;
settings: RuleSettings;
}
export interface Pipeline {
rules: Rule[];
}
export interface GrafanaCloudBackend {
uid: string;
settings: any;
}
export type RuleType = 'converter' | 'frameProcessors' | 'frameOutputs';
export interface PipelineListOption {
type: string;
description: string;
example?: object;
}
export interface EntitiesTypes {
converters: PipelineListOption[];
frameProcessors: PipelineListOption[];
frameOutputs: PipelineListOption[];
}
export interface PipeLineEntitiesInfo {
converter: SelectableValue[];
frameProcessors: SelectableValue[];
frameOutputs: SelectableValue[];
getExample: (rule: RuleType, type: string) => object;
}
export interface ChannelFrame {
channel: string;
frame: DataFrame;
}

@ -1,31 +0,0 @@
import { getBackendSrv } from '@grafana/runtime';
import { PipelineListOption, PipeLineEntitiesInfo } from './types';
export async function getPipeLineEntities(): Promise<PipeLineEntitiesInfo> {
return await getBackendSrv()
.get(`api/live/pipeline-entities`)
.then((data) => {
return {
converter: transformLabel(data, 'converters'),
frameProcessors: transformLabel(data, 'frameProcessors'),
frameOutputs: transformLabel(data, 'frameOutputs'),
getExample: (ruleType, type) => {
return data[`${ruleType}s`]?.filter((option: PipelineListOption) => option.type === type)?.[0]?.['example'];
},
};
});
}
export function transformLabel(data: any, key: keyof typeof data) {
if (Array.isArray(data)) {
return data.map((d) => ({
label: d[key],
value: d[key],
}));
}
return data[key].map((typeObj: PipelineListOption) => ({
label: typeObj.type,
value: typeObj.type,
}));
}

@ -1,132 +0,0 @@
/* Do not change, this code is generated from Golang structs */
import { FieldConfig } from '@grafana/data';
export interface ChannelAuthCheckConfig {
role?: string;
}
export interface ChannelAuthConfig {
subscribe?: ChannelAuthCheckConfig;
publish?: ChannelAuthCheckConfig;
}
export interface ChangeLogOutputConfig {
fieldName: string;
channel: string;
}
export interface RemoteWriteOutputConfig {
uid: string;
sampleMilliseconds: number;
}
export interface ThresholdOutputConfig {
fieldName: string;
channel: string;
}
export interface NumberCompareFrameConditionConfig {
fieldName: string;
op: string;
value: number;
}
export interface MultipleFrameConditionCheckerConfig {
conditionType: string;
conditions: FrameConditionCheckerConfig[];
}
export interface FrameConditionCheckerConfig {
type: Omit<keyof FrameConditionCheckerConfig, 'type'>;
multiple?: MultipleFrameConditionCheckerConfig;
numberCompare?: NumberCompareFrameConditionConfig;
}
export interface ConditionalOutputConfig {
condition?: FrameConditionCheckerConfig;
output?: FrameOutputterConfig;
}
export interface RedirectOutputConfig {
channel: string;
}
export interface MultipleOutputterConfig {
outputs: FrameOutputterConfig[];
}
export interface ManagedStreamOutputConfig {}
export interface FrameOutputterConfig {
type: Omit<keyof FrameOutputterConfig, 'type'>;
managedStream?: ManagedStreamOutputConfig;
multiple?: MultipleOutputterConfig;
redirect?: RedirectOutputConfig;
conditional?: ConditionalOutputConfig;
threshold?: ThresholdOutputConfig;
remoteWrite?: RemoteWriteOutputConfig;
loki?: LokiOutputConfig;
changeLog?: ChangeLogOutputConfig;
}
export interface MultipleFrameProcessorConfig {
processors: FrameProcessorConfig[];
}
export interface KeepFieldsFrameProcessorConfig {
fieldNames: string[];
}
export interface DropFieldsFrameProcessorConfig {
fieldNames: string[];
}
export interface FrameProcessorConfig {
type: Omit<keyof FrameProcessorConfig, 'type'>;
dropFields?: DropFieldsFrameProcessorConfig;
keepFields?: KeepFieldsFrameProcessorConfig;
multiple?: MultipleFrameProcessorConfig;
}
export interface JsonFrameConverterConfig {}
export interface AutoInfluxConverterConfig {
frameFormat: string;
}
export interface ExactJsonConverterConfig {
fields: Field[];
}
export interface Label {
name: string;
value: string;
}
export interface Field {
name: string;
type: number;
value: string;
labels?: Label[];
config?: FieldConfig;
}
export interface AutoJsonConverterConfig {
fieldTips?: { [key: string]: Field };
}
export interface ConverterConfig {
type: Omit<keyof ConverterConfig, 'type'>;
jsonAuto?: AutoJsonConverterConfig;
jsonExact?: ExactJsonConverterConfig;
influxAuto?: AutoInfluxConverterConfig;
jsonFrame?: JsonFrameConverterConfig;
}
export interface LokiOutputConfig {
uid: string;
}
export interface RedirectDataOutputConfig {
channel: string;
}
export interface DataOutputterConfig {
type: Omit<keyof DataOutputterConfig, 'type'>;
redirect?: RedirectDataOutputConfig;
loki?: LokiOutputConfig;
}
export interface MultipleSubscriberConfig {
subscribers: SubscriberConfig[];
}
export interface SubscriberConfig {
type: Omit<keyof SubscriberConfig, 'type'>;
multiple?: MultipleSubscriberConfig;
}
export interface ChannelRuleSettings {
auth?: ChannelAuthConfig;
subscribers?: SubscriberConfig[];
dataOutputs?: DataOutputterConfig[];
converter?: ConverterConfig;
frameProcessors?: FrameProcessorConfig[];
frameOutputs?: FrameOutputterConfig[];
}
export interface ChannelRule {
pattern: string;
settings: ChannelRuleSettings;
}

@ -11,7 +11,6 @@ import LdapPage from 'app/features/admin/ldap/LdapPage';
import { getAlertingRoutes } from 'app/features/alerting/routes'; import { getAlertingRoutes } from 'app/features/alerting/routes';
import { getRoutes as getDataConnectionsRoutes } from 'app/features/connections/routes'; import { getRoutes as getDataConnectionsRoutes } from 'app/features/connections/routes';
import { DATASOURCES_ROUTES } from 'app/features/datasources/constants'; import { DATASOURCES_ROUTES } from 'app/features/datasources/constants';
import { getLiveRoutes } from 'app/features/live/pages/routes';
import { getRoutes as getPluginCatalogRoutes } from 'app/features/plugins/admin/routes'; import { getRoutes as getPluginCatalogRoutes } from 'app/features/plugins/admin/routes';
import { getAppPluginRoutes } from 'app/features/plugins/routes'; import { getAppPluginRoutes } from 'app/features/plugins/routes';
import { getProfileRoutes } from 'app/features/profile/routes'; import { getProfileRoutes } from 'app/features/profile/routes';
@ -503,7 +502,6 @@ export function getAppRoutes(): RouteDescriptor[] {
...getDynamicDashboardRoutes(), ...getDynamicDashboardRoutes(),
...getPluginCatalogRoutes(), ...getPluginCatalogRoutes(),
...getSupportBundleRoutes(), ...getSupportBundleRoutes(),
...getLiveRoutes(),
...getAlertingRoutes(), ...getAlertingRoutes(),
...getProfileRoutes(), ...getProfileRoutes(),
...extraRoutes, ...extraRoutes,

@ -243,18 +243,6 @@
"subtitle": "Wiederverwendbare Panels, die zu mehreren Dashboards hinzugefügt werden können", "subtitle": "Wiederverwendbare Panels, die zu mehreren Dashboards hinzugefügt werden können",
"title": "Bibliotheks-Panels" "title": "Bibliotheks-Panels"
}, },
"live": {
"title": "Event-Streaming"
},
"live-cloud": {
"title": "Cloud"
},
"live-pipeline": {
"title": "In Vorbereitung"
},
"live-status": {
"title": "Status"
},
"manage-dashboards": { "manage-dashboards": {
"title": "Durchsuchen" "title": "Durchsuchen"
}, },

@ -243,18 +243,6 @@
"subtitle": "Reusable panels that can be added to multiple dashboards", "subtitle": "Reusable panels that can be added to multiple dashboards",
"title": "Library panels" "title": "Library panels"
}, },
"live": {
"title": "Event streaming"
},
"live-cloud": {
"title": "Cloud"
},
"live-pipeline": {
"title": "Pipeline"
},
"live-status": {
"title": "Status"
},
"manage-dashboards": { "manage-dashboards": {
"title": "Browse" "title": "Browse"
}, },

@ -243,18 +243,6 @@
"subtitle": "Paneles reutilizables que se pueden añadir a varios paneles de control", "subtitle": "Paneles reutilizables que se pueden añadir a varios paneles de control",
"title": "Paneles de la librería" "title": "Paneles de la librería"
}, },
"live": {
"title": "Transmisión de eventos"
},
"live-cloud": {
"title": "Nube"
},
"live-pipeline": {
"title": "Cartera"
},
"live-status": {
"title": "Estado"
},
"manage-dashboards": { "manage-dashboards": {
"title": "Navegar" "title": "Navegar"
}, },

@ -243,18 +243,6 @@
"subtitle": "Panneaux réutilisables pouvant être ajoutés à plusieurs tableaux de bord", "subtitle": "Panneaux réutilisables pouvant être ajoutés à plusieurs tableaux de bord",
"title": "Panneaux de bibliothèque" "title": "Panneaux de bibliothèque"
}, },
"live": {
"title": "Diffusion d'événements"
},
"live-cloud": {
"title": "Cloud"
},
"live-pipeline": {
"title": "Pipeline"
},
"live-status": {
"title": "Statut"
},
"manage-dashboards": { "manage-dashboards": {
"title": "Parcourir" "title": "Parcourir"
}, },

@ -243,18 +243,6 @@
"subtitle": "Ŗęūşäþľę päʼnęľş ŧĥäŧ čäʼn þę äđđęđ ŧő mūľŧįpľę đäşĥþőäřđş", "subtitle": "Ŗęūşäþľę päʼnęľş ŧĥäŧ čäʼn þę äđđęđ ŧő mūľŧįpľę đäşĥþőäřđş",
"title": "Ŀįþřäřy päʼnęľş" "title": "Ŀįþřäřy päʼnęľş"
}, },
"live": {
"title": "Ēvęʼnŧ şŧřęämįʼnģ"
},
"live-cloud": {
"title": "Cľőūđ"
},
"live-pipeline": {
"title": "Pįpęľįʼnę"
},
"live-status": {
"title": "Ŝŧäŧūş"
},
"manage-dashboards": { "manage-dashboards": {
"title": "ßřőŵşę" "title": "ßřőŵşę"
}, },

@ -243,18 +243,6 @@
"subtitle": "可重复使用的面板,可以添加到多个仪表板", "subtitle": "可重复使用的面板,可以添加到多个仪表板",
"title": "库面板" "title": "库面板"
}, },
"live": {
"title": "事件流"
},
"live-cloud": {
"title": "云"
},
"live-pipeline": {
"title": "管道"
},
"live-status": {
"title": "状态"
},
"manage-dashboards": { "manage-dashboards": {
"title": "浏览" "title": "浏览"
}, },

Loading…
Cancel
Save