From 4680a8454f9ca03f07a9322f7ae5753be8d851fe Mon Sep 17 00:00:00 2001 From: Ryan McKinley Date: Fri, 22 Oct 2021 08:56:16 -0700 Subject: [PATCH] Live: add rule for datasource (#40738) --- pkg/api/index.go | 4 +- public/app/features/live/pages/AddNewRule.tsx | 144 +++++++++++++----- .../features/live/pages/PipelineAdminPage.tsx | 120 +++------------ .../app/features/live/pages/PipelineTable.tsx | 144 ++++++++++++++++++ public/app/features/live/pages/RuleModal.tsx | 83 +++++----- public/app/features/live/pages/RuleTest.tsx | 93 ++++------- public/app/features/live/pages/routes.ts | 6 - 7 files changed, 355 insertions(+), 239 deletions(-) create mode 100644 public/app/features/live/pages/PipelineTable.tsx diff --git a/pkg/api/index.go b/pkg/api/index.go index c6431fc5ead..9d763d10a7b 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -317,9 +317,6 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto liveNavLinks = append(liveNavLinks, &dtos.NavLink{ Text: "Cloud", Id: "live-cloud", Url: hs.Cfg.AppSubURL + "/live/cloud", Icon: "cloud-upload", }) - liveNavLinks = append(liveNavLinks, &dtos.NavLink{ - Text: "Test", Id: "live-test", Url: hs.Cfg.AppSubURL + "/live/test", Icon: "arrow", - }) navTree = append(navTree, &dtos.NavLink{ Id: "live", Text: "Live", @@ -328,6 +325,7 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto Url: hs.Cfg.AppSubURL + "/live", Children: liveNavLinks, HideFromMenu: true, + HideFromTabs: true, }) } diff --git a/public/app/features/live/pages/AddNewRule.tsx b/public/app/features/live/pages/AddNewRule.tsx index 78844a8ebea..0af3926fb9f 100644 --- a/public/app/features/live/pages/AddNewRule.tsx +++ b/public/app/features/live/pages/AddNewRule.tsx @@ -1,32 +1,48 @@ -import React from 'react'; -import { Input, Form, Field, Button } from '@grafana/ui'; -import { getBackendSrv } from '@grafana/runtime'; +import React, { useState } from 'react'; +import { Input, Field, Button, ValuePicker, HorizontalGroup } from '@grafana/ui'; +import { DataSourcePicker, getBackendSrv } from '@grafana/runtime'; +import { AppEvents, DatasourceRef, LiveChannelScope, SelectableValue } from '@grafana/data'; +import appEvents from 'app/core/app_events'; import { Rule } from './types'; interface Props { - onClose: (state: boolean) => void; + onRuleAdded: (rule: Rule) => void; } -export function AddNewRule({ onClose }: Props) { - const onSubmit = (formData: Rule) => { + +type PatternType = 'ds' | 'any'; + +const patternTypes: Array> = [ + { + 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(); + const [pattern, setPattern] = useState(); + const [patternPrefix, setPatternPrefix] = useState(''); + const [datasource, setDatasource] = useState(); + + const onSubmit = () => { + if (!pattern) { + appEvents.emit(AppEvents.alertError, ['Enter path']); + return; + } + if (patternType === 'ds' && !patternPrefix.length) { + appEvents.emit(AppEvents.alertError, ['Select datasource']); + return; + } + getBackendSrv() .post(`api/live/channel-rules`, { - pattern: formData.pattern, - settings: { - output: formData.settings.frameOutputs, - converter: formData.settings.converter, - }, - }) - .then(() => { - // close modal - onClose(false); - }) - .catch((e) => console.error(e)); - }; - - return ( -
- {({ register, errors }) => ( - <> - - + }) + .then((v: any) => { + console.log('ADDED', v); + setPattern(undefined); + setPatternType(undefined); + onRuleAdded(v.rule); + }) + .catch((e) => { + appEvents.emit(AppEvents.alertError, ['Error adding rule', e]); + e.isHandled = true; + }); + }; + + if (patternType) { + return ( +
+ + {patternType === 'any' && ( + + setPattern(e.currentTarget.value)} + placeholder="scope/namespace/path" + /> + + )} + {patternType === 'ds' && ( + <> + + { + setDatasource(ds.name); + setPatternPrefix(`${LiveChannelScope.DataSource}/${ds.uid}/`); + }} + /> + + + setPattern(e.currentTarget.value)} placeholder="path" /> + + + )} + + + - - - )} - + + + + + +
+ ); + } + + return ( +
+ setPatternType(v.value)} + /> +
); } diff --git a/public/app/features/live/pages/PipelineAdminPage.tsx b/public/app/features/live/pages/PipelineAdminPage.tsx index 99bdf32b2fd..2edda347c00 100644 --- a/public/app/features/live/pages/PipelineAdminPage.tsx +++ b/public/app/features/live/pages/PipelineAdminPage.tsx @@ -1,33 +1,20 @@ import React, { useEffect, useState, ChangeEvent } from 'react'; import { getBackendSrv } from '@grafana/runtime'; -import { Input, Tag, useStyles, Button, Modal, IconButton } from '@grafana/ui'; +import { Input } from '@grafana/ui'; import Page from 'app/core/components/Page/Page'; import { useNavModel } from 'app/core/hooks/useNavModel'; -import { css } from '@emotion/css'; -import { GrafanaTheme } from '@grafana/data'; -import { Rule, Output, RuleType } from './types'; -import { RuleModal } from './RuleModal'; +import { Rule } from './types'; +import { PipelineTable } from './PipelineTable'; import { AddNewRule } from './AddNewRule'; -function renderOutputTags(key: string, output?: Output): React.ReactNode { - if (!output?.type) { - return null; - } - return ; -} - export default function PipelineAdminPage() { const [rules, setRules] = useState([]); - const [isOpen, setOpen] = useState(false); - const [selectedRule, setSelectedRule] = useState(); const [defaultRules, setDefaultRules] = useState([]); + const [newRule, setNewRule] = useState(); const navModel = useNavModel('live-pipeline'); - const [isOpenEditor, setOpenEditor] = useState(false); const [error, setError] = useState(); - const [clickColumn, setClickColumn] = useState('converter'); - const styles = useStyles(getStyles); - useEffect(() => { + const loadRules = () => { getBackendSrv() .get(`api/live/channel-rules`) .then((data) => { @@ -39,20 +26,12 @@ export default function PipelineAdminPage() { setError(JSON.stringify(e.data, null, 2)); } }); - }, [isOpenEditor, isOpen]); - - const onRowClick = (event: any) => { - const pattern = event.target.getAttribute('data-pattern'); - const column = event.target.getAttribute('data-column'); - if (column === 'pattern') { - setClickColumn('converter'); - } else { - setClickColumn(column); - } - setSelectedRule(rules.filter((rule) => rule.pattern === pattern)[0]); - setOpen(true); }; + useEffect(() => { + loadRules(); + }, []); + const onSearchQueryChange = (e: ChangeEvent) => { if (e.target.value) { setRules(rules.filter((rule) => rule.pattern.toLowerCase().includes(e.target.value.toLowerCase()))); @@ -61,11 +40,6 @@ export default function PipelineAdminPage() { } }; - const onRemoveRule = (pattern: string) => { - getBackendSrv() - .delete(`api/live/channel-rules`, JSON.stringify({ pattern: pattern })) - .catch((e) => console.error(e)); - }; return ( @@ -73,75 +47,19 @@ export default function PipelineAdminPage() {
-
-
- - - - - - - - - - - {rules.map((rule) => ( - - - - - - - - ))} - -
PatternConverterProcessorOutput
- {rule.pattern} - - {rule.settings?.converter?.type} - - {rule.settings?.frameProcessors?.map((processor) => ( - {processor.type} - ))} - - {rule.settings?.frameOutputs?.map((output) => ( - {renderOutputTags('out', output)} - ))} - - onRemoveRule(rule.pattern)}> -
-
- {isOpenEditor && ( - setOpenEditor(false)} title="Add a new rule"> - - - )} - {isOpen && selectedRule && ( - { - setOpen(false); - }} - clickColumn={clickColumn} - /> - )} + + + + { + console.log('GOT', r, 'vs', rules[0]); + setNewRule(r); + loadRules(); + }} + />
); } - -const getStyles = (theme: GrafanaTheme) => { - return { - row: css` - cursor: pointer; - `, - addNew: css` - margin-left: 10px; - `, - }; -}; diff --git a/public/app/features/live/pages/PipelineTable.tsx b/public/app/features/live/pages/PipelineTable.tsx new file mode 100644 index 00000000000..32873a40c16 --- /dev/null +++ b/public/app/features/live/pages/PipelineTable.tsx @@ -0,0 +1,144 @@ +import React, { useEffect, useState } from 'react'; +import { getBackendSrv } from '@grafana/runtime'; +import { Tag, useStyles, IconButton } from '@grafana/ui'; +import { css } from '@emotion/css'; +import { GrafanaTheme } from '@grafana/data'; +import { Rule, Output, RuleType } from './types'; +import { RuleModal } from './RuleModal'; +import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; + +function renderOutputTags(key: string, output?: Output): React.ReactNode { + if (!output?.type) { + return null; + } + return ; +} + +interface Props { + rules: Rule[]; + onRuleChanged: () => void; + selectRule?: Rule; +} + +export const PipelineTable: React.FC = (props) => { + const { rules } = props; + const [isOpen, setOpen] = useState(false); + const [selectedRule, setSelectedRule] = useState(); + const [clickColumn, setClickColumn] = useState('converter'); + const styles = useStyles(getStyles); + + 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 ( +
+   + {pattern.substring(idx + 1)} +
+ ); + } + } + } + return pattern; + }; + + return ( +
+
+ + + + + + + + + + + + {rules.map((rule) => ( + onRowClick(rule, e)} className={styles.row}> + + + + + + + ))} + +
ChannelConverterProcessorOutput 
+ {renderPattern(rule.pattern)} + + {rule.settings?.converter?.type} + + {rule.settings?.frameProcessors?.map((processor) => ( + {processor.type} + ))} + + {rule.settings?.frameOutputs?.map((output) => ( + {renderOutputTags('out', output)} + ))} + + { + e.stopPropagation(); + onRemoveRule(rule.pattern); + }} + > +
+
+ {isOpen && selectedRule && ( + { + setOpen(false); + }} + clickColumn={clickColumn} + /> + )} +
+ ); +}; + +const getStyles = (theme: GrafanaTheme) => { + return { + row: css` + cursor: pointer; + `, + }; +}; diff --git a/public/app/features/live/pages/RuleModal.tsx b/public/app/features/live/pages/RuleModal.tsx index 605a564da1f..0f2686024cb 100644 --- a/public/app/features/live/pages/RuleModal.tsx +++ b/public/app/features/live/pages/RuleModal.tsx @@ -7,6 +7,7 @@ import { GrafanaTheme } from '@grafana/data'; import { RuleSettingsEditor } from './RuleSettingsEditor'; import { getPipeLineEntities } from './utils'; import { RuleSettingsArray } from './RuleSettingsArray'; +import { RuleTest } from './RuleTest'; interface Props { rule: Rule; @@ -14,35 +15,41 @@ interface Props { onClose: () => void; clickColumn: RuleType; } -interface TabType { +interface TabInfo { label: string; - value: RuleType; + type?: RuleType; + isTest?: boolean; + isConverter?: boolean; + icon?: string; } -const tabs: TabType[] = [ - { label: 'Converter', value: 'converter' }, - { label: 'Processors', value: 'frameProcessors' }, - { label: 'Outputs', value: 'frameOutputs' }, +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: React.FC = (props) => { const { isOpen, onClose, clickColumn } = props; const [rule, setRule] = useState(props.rule); - const [activeTab, setActiveTab] = useState(clickColumn); + const [activeTab, setActiveTab] = useState(tabs.find((t) => t.type === clickColumn)); // to show color of Save button const [hasChange, setChange] = useState(false); - const [ruleSetting, setRuleSetting] = useState(rule?.settings?.[activeTab]); + const [ruleSetting, setRuleSetting] = useState(activeTab?.type ? rule?.settings?.[activeTab.type] : undefined); const [entitiesInfo, setEntitiesInfo] = useState(); const styles = useStyles(getStyles); const onRuleSettingChange = (value: RuleSetting | RuleSetting[]) => { setChange(true); - setRule({ - ...rule, - settings: { - ...rule.settings, - [activeTab]: value, - }, - }); + if (activeTab?.type) { + setRule({ + ...rule, + settings: { + ...rule.settings, + [activeTab?.type]: value, + }, + }); + } setRuleSetting(value); }; @@ -71,32 +78,40 @@ export const RuleModal: React.FC = (props) => { { - setActiveTab(tab.value); - // to notify children of the new rule - setRuleSetting(rule?.settings?.[tab.value]); + setActiveTab(tab); + if (tab.type) { + // to notify children of the new rule + setRuleSetting(rule?.settings?.[tab.type]); + } }} /> ); })} - {entitiesInfo && rule && activeTab === 'converter' && ( - - )} - {entitiesInfo && rule && activeTab !== 'converter' && ( - + {entitiesInfo && rule && activeTab && ( + <> + {activeTab?.isTest && } + {activeTab.isConverter && ( + + )} + {!activeTab.isConverter && activeTab.type && ( + + )} + )}