mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
add: rule settings editor (#39875)
This commit is contained in:
parent
526961f298
commit
ecf9733a66
51
public/app/features/live/pages/AddNewRule.tsx
Normal file
51
public/app/features/live/pages/AddNewRule.tsx
Normal file
@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import { Input, Form, Field, Button } from '@grafana/ui';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { Rule } from './types';
|
||||
|
||||
interface Props {
|
||||
onClose: (state: boolean) => void;
|
||||
}
|
||||
export function AddNewRule({ onClose }: Props) {
|
||||
const onSubmit = (formData: Rule) => {
|
||||
getBackendSrv()
|
||||
.post(`api/live/channel-rules`, {
|
||||
pattern: formData.pattern,
|
||||
settings: {
|
||||
output: formData.settings.output,
|
||||
converter: formData.settings.converter,
|
||||
},
|
||||
})
|
||||
.then(() => {
|
||||
// close modal
|
||||
onClose(false);
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
};
|
||||
|
||||
return (
|
||||
<Form
|
||||
defaultValues={{
|
||||
pattern: '',
|
||||
settings: {
|
||||
converter: {
|
||||
type: 'jsonAuto',
|
||||
},
|
||||
output: {
|
||||
type: 'managedStream',
|
||||
},
|
||||
},
|
||||
}}
|
||||
onSubmit={onSubmit}
|
||||
>
|
||||
{({ register, errors }) => (
|
||||
<>
|
||||
<Field label="Pattern" invalid={!!errors.pattern} error="Pattern is required">
|
||||
<Input {...register('pattern', { required: true })} placeholder="scope/namespace/path" />
|
||||
</Field>
|
||||
<Button>Add</Button>
|
||||
</>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
}
|
@ -1,12 +1,13 @@
|
||||
import React, { useEffect, useState, ChangeEvent } from 'react';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { Input, Tag, useStyles } from '@grafana/ui';
|
||||
import { Input, Tag, useStyles, Button, Modal, IconButton } 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 } from './types';
|
||||
import { Rule, Output, RuleType } from './types';
|
||||
import { RuleModal } from './RuleModal';
|
||||
import { AddNewRule } from './AddNewRule';
|
||||
|
||||
function renderOutputTags(key: string, output?: Output): React.ReactNode {
|
||||
if (!output?.type) {
|
||||
@ -24,7 +25,9 @@ export default function PipelineAdminPage() {
|
||||
const [selectedRule, setSelectedRule] = useState<Rule>();
|
||||
const [defaultRules, setDefaultRules] = useState<any[]>([]);
|
||||
const navModel = useNavModel('live-pipeline');
|
||||
const [isOpenEditor, setOpenEditor] = useState<boolean>(false);
|
||||
const [error, setError] = useState<string>();
|
||||
const [clickColumn, setClickColumn] = useState<RuleType>('converter');
|
||||
const styles = useStyles(getStyles);
|
||||
|
||||
useEffect(() => {
|
||||
@ -39,13 +42,14 @@ 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');
|
||||
console.log('show:', column);
|
||||
// setActiveTab(column);
|
||||
if (column) {
|
||||
setClickColumn(column);
|
||||
}
|
||||
setSelectedRule(rules.filter((rule) => rule.pattern === pattern)[0]);
|
||||
setOpen(true);
|
||||
};
|
||||
@ -53,12 +57,16 @@ export default function PipelineAdminPage() {
|
||||
const onSearchQueryChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.value) {
|
||||
setRules(rules.filter((rule) => rule.pattern.toLowerCase().includes(e.target.value.toLowerCase())));
|
||||
console.log(e.target.value, rules);
|
||||
} else {
|
||||
setRules(defaultRules);
|
||||
}
|
||||
};
|
||||
|
||||
const onRemoveRule = (pattern: string) => {
|
||||
getBackendSrv()
|
||||
.delete(`api/live/channel-rules`, JSON.stringify({ pattern: pattern }))
|
||||
.catch((e) => console.error(e));
|
||||
};
|
||||
return (
|
||||
<Page navModel={navModel}>
|
||||
<Page.Contents>
|
||||
@ -66,6 +74,9 @@ export default function PipelineAdminPage() {
|
||||
<div className="page-action-bar">
|
||||
<div className="gf-form gf-form--grow">
|
||||
<Input placeholder="Search pattern..." onChange={onSearchQueryChange} />
|
||||
<Button className={styles.addNew} onClick={() => setOpenEditor(true)}>
|
||||
Add Rule
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="admin-list-table">
|
||||
@ -93,12 +104,29 @@ export default function PipelineAdminPage() {
|
||||
<td data-pattern={rule.pattern} data-column="output">
|
||||
{renderOutputTags('out', rule.settings?.output)}
|
||||
</td>
|
||||
<td>
|
||||
<IconButton name="trash-alt" onClick={() => onRemoveRule(rule.pattern)}></IconButton>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{isOpen && selectedRule && <RuleModal rule={selectedRule} isOpen={isOpen} onClose={() => setOpen(false)} />}
|
||||
{isOpenEditor && (
|
||||
<Modal isOpen={isOpenEditor} onDismiss={() => setOpenEditor(false)} title="Add a new rule">
|
||||
<AddNewRule onClose={setOpenEditor} />
|
||||
</Modal>
|
||||
)}
|
||||
{isOpen && selectedRule && (
|
||||
<RuleModal
|
||||
rule={selectedRule}
|
||||
isOpen={isOpen}
|
||||
onClose={() => {
|
||||
setOpen(false);
|
||||
}}
|
||||
clickColumn={clickColumn}
|
||||
/>
|
||||
)}
|
||||
</Page.Contents>
|
||||
</Page>
|
||||
);
|
||||
@ -109,5 +137,8 @@ const getStyles = (theme: GrafanaTheme) => {
|
||||
row: css`
|
||||
cursor: pointer;
|
||||
`,
|
||||
addNew: css`
|
||||
margin-left: 10px;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
@ -1,23 +1,65 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Modal, TabContent, TabsBar, Tab, CodeEditor } from '@grafana/ui';
|
||||
import { Rule } from './types';
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { Modal, TabContent, TabsBar, Tab, Button, useStyles } from '@grafana/ui';
|
||||
import { Rule, RuleType, PipeLineEntitiesInfo, RuleSetting } from './types';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { css } from '@emotion/css';
|
||||
import { GrafanaTheme } from '@grafana/data';
|
||||
import { RuleSettingsEditor } from './RuleSettingsEditor';
|
||||
import { getPipeLineEntities } from './utils';
|
||||
|
||||
interface Props {
|
||||
rule: Rule;
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
clickColumn: RuleType;
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
interface TabType {
|
||||
label: string;
|
||||
value: RuleType;
|
||||
}
|
||||
const tabs: TabType[] = [
|
||||
{ label: 'Converter', value: 'converter' },
|
||||
{ label: 'Processor', value: 'processor' },
|
||||
{ label: 'Output', value: 'output' },
|
||||
];
|
||||
const height = 600;
|
||||
|
||||
export const RuleModal: React.FC<Props> = (props) => {
|
||||
const { rule, isOpen, onClose } = props;
|
||||
const [activeTab, setActiveTab] = useState<string>('converter');
|
||||
const { isOpen, onClose, clickColumn } = props;
|
||||
const [rule, setRule] = useState<Rule>(props.rule);
|
||||
const [activeTab, setActiveTab] = useState<RuleType>(clickColumn);
|
||||
// to show color of Save button
|
||||
const [hasChange, setChange] = useState<boolean>(false);
|
||||
const [ruleSetting, setRuleSetting] = useState<any>(rule?.settings?.[activeTab]);
|
||||
const [entitiesInfo, setEntitiesInfo] = useState<PipeLineEntitiesInfo>();
|
||||
const styles = useStyles(getStyles);
|
||||
|
||||
const onRuleSettingChange = (value: RuleSetting) => {
|
||||
setChange(true);
|
||||
setRule({
|
||||
...rule,
|
||||
settings: {
|
||||
...rule.settings,
|
||||
[activeTab]: 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);
|
||||
})
|
||||
.catch((e) => console.error(e));
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} title={rule.pattern} onDismiss={onClose} closeOnEscape>
|
||||
@ -30,70 +72,34 @@ export const RuleModal: React.FC<Props> = (props) => {
|
||||
active={tab.value === activeTab}
|
||||
onChangeTab={() => {
|
||||
setActiveTab(tab.value);
|
||||
// to notify children of the new rule
|
||||
setRuleSetting(rule?.settings?.[tab.value]);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TabsBar>
|
||||
<TabContent>
|
||||
{activeTab === 'converter' && <ConverterEditor {...props} />}
|
||||
{activeTab === 'processor' && <ProcessorEditor {...props} />}
|
||||
{activeTab === 'output' && <OutputEditor {...props} />}
|
||||
{entitiesInfo && rule && (
|
||||
<RuleSettingsEditor
|
||||
onChange={onRuleSettingChange}
|
||||
value={ruleSetting}
|
||||
ruleType={activeTab}
|
||||
entitiesInfo={entitiesInfo}
|
||||
/>
|
||||
)}
|
||||
<Button onClick={onSave} className={styles.save} variant={hasChange ? 'primary' : 'secondary'}>
|
||||
Save
|
||||
</Button>
|
||||
</TabContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export const ConverterEditor: React.FC<Props> = ({ rule }) => {
|
||||
const { converter } = rule.settings;
|
||||
if (!converter) {
|
||||
return <div>No converter defined</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<CodeEditor
|
||||
height={height}
|
||||
value={JSON.stringify(converter, null, '\t')}
|
||||
showLineNumbers={true}
|
||||
readOnly={true}
|
||||
language="json"
|
||||
showMiniMap={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const ProcessorEditor: React.FC<Props> = ({ rule }) => {
|
||||
const { processor } = rule.settings;
|
||||
if (!processor) {
|
||||
return <div>No processor defined</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<CodeEditor
|
||||
height={height}
|
||||
value={JSON.stringify(processor, null, '\t')}
|
||||
showLineNumbers={true}
|
||||
readOnly={true}
|
||||
language="json"
|
||||
showMiniMap={false}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const OutputEditor: React.FC<Props> = ({ rule }) => {
|
||||
const { output } = rule.settings;
|
||||
if (!output) {
|
||||
return <div>No output defined</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<CodeEditor
|
||||
height={height}
|
||||
value={JSON.stringify(output, null, '\t')}
|
||||
showLineNumbers={true}
|
||||
readOnly={true}
|
||||
language="json"
|
||||
showMiniMap={false}
|
||||
/>
|
||||
);
|
||||
const getStyles = (theme: GrafanaTheme) => {
|
||||
return {
|
||||
save: css`
|
||||
margin-top: 5px;
|
||||
`,
|
||||
};
|
||||
};
|
||||
|
47
public/app/features/live/pages/RuleSettingsEditor.tsx
Normal file
47
public/app/features/live/pages/RuleSettingsEditor.tsx
Normal file
@ -0,0 +1,47 @@
|
||||
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: React.FC<Props> = ({ onChange, value, ruleType, entitiesInfo }) => {
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
menuShouldPortal={true}
|
||||
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,21 +1,23 @@
|
||||
export interface Converter {
|
||||
type: string;
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
export interface Converter extends RuleSetting {
|
||||
[t: string]: any;
|
||||
}
|
||||
|
||||
export interface Processor {
|
||||
type: string;
|
||||
export interface Processor extends RuleSetting {
|
||||
[t: string]: any;
|
||||
}
|
||||
|
||||
export interface Output {
|
||||
type: string;
|
||||
export interface Output extends RuleSetting {
|
||||
[t: string]: any;
|
||||
multiple?: {
|
||||
outputs: Output[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface RuleSetting<T = any> {
|
||||
type: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
export interface RuleSettings {
|
||||
converter?: Converter;
|
||||
processor?: Processor;
|
||||
@ -35,3 +37,23 @@ export interface GrafanaCloudBackend {
|
||||
uid: string;
|
||||
settings: any;
|
||||
}
|
||||
|
||||
export type RuleType = 'converter' | 'processor' | 'output';
|
||||
|
||||
export interface PipelineListOption {
|
||||
type: string;
|
||||
description: string;
|
||||
example: object;
|
||||
}
|
||||
export interface EntitiesTypes {
|
||||
converters: PipelineListOption[];
|
||||
processors: PipelineListOption[];
|
||||
outputs: PipelineListOption[];
|
||||
}
|
||||
|
||||
export interface PipeLineEntitiesInfo {
|
||||
converter: SelectableValue[];
|
||||
processor: SelectableValue[];
|
||||
output: SelectableValue[];
|
||||
getExample: (rule: RuleType, type: string) => object;
|
||||
}
|
||||
|
24
public/app/features/live/pages/utils.ts
Normal file
24
public/app/features/live/pages/utils.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
import { PipelineListOption, EntitiesTypes, PipeLineEntitiesInfo } from './types';
|
||||
|
||||
export async function getPipeLineEntities(): Promise<PipeLineEntitiesInfo> {
|
||||
return await getBackendSrv()
|
||||
.get(`api/live/pipeline-entities`)
|
||||
.then((data) => {
|
||||
return {
|
||||
converter: transformLabel(data, 'converters'),
|
||||
processor: transformLabel(data, 'processors'),
|
||||
output: transformLabel(data, 'outputs'),
|
||||
getExample: (ruleType, type) => {
|
||||
return data[`${ruleType}s`]?.filter((option: PipelineListOption) => option.type === type)?.[0]?.['example'];
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function transformLabel(data: EntitiesTypes, key: keyof typeof data) {
|
||||
return data[key].map((typeObj: PipelineListOption) => ({
|
||||
label: typeObj.type,
|
||||
value: typeObj.type,
|
||||
}));
|
||||
}
|
Loading…
Reference in New Issue
Block a user