add: rule settings editor (#39875)

This commit is contained in:
An 2021-09-30 16:37:31 -04:00 committed by GitHub
parent 526961f298
commit ecf9733a66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 257 additions and 76 deletions

View 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>
);
}

View File

@ -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;
`,
};
};

View File

@ -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;
`,
};
};

View 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,
});
}}
/>
</>
);
};

View File

@ -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;
}

View 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,
}));
}