Live: admin config UI (#39103)

Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
Co-authored-by: Atif Ali <atifshoukatali@yahoo.com>
This commit is contained in:
An 2021-09-14 06:09:55 +02:00 committed by GitHub
parent d3a7e0228c
commit 45e67630e8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 395 additions and 1 deletions

View File

@ -72,9 +72,12 @@ func (hs *HTTPServer) registerRoutes() {
r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, hs.Index)
r.Get("/admin/stats", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionServerStatsRead)), hs.Index)
r.Get("/admin/ldap", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPStatusRead)), hs.Index)
r.Get("/styleguide", reqSignedIn, hs.Index)
r.Get("/live", reqGrafanaAdmin, hs.Index)
r.Get("/live/pipeline", reqGrafanaAdmin, hs.Index)
r.Get("/live/cloud", reqGrafanaAdmin, hs.Index)
r.Get("/plugins", reqSignedIn, hs.Index)
r.Get("/plugins/:id/", reqSignedIn, hs.Index)
r.Get("/plugins/:id/edit", reqSignedIn, hs.Index) // deprecated

View File

@ -318,6 +318,30 @@ func (hs *HTTPServer) getNavTree(c *models.ReqContext, hasEditPerm bool) ([]*dto
})
}
if true {
liveNavLinks := []*dtos.NavLink{}
liveNavLinks = append(liveNavLinks, &dtos.NavLink{
Text: "Status", Id: "live-status", Url: hs.Cfg.AppSubURL + "/live", Icon: "exchange-alt",
})
liveNavLinks = append(liveNavLinks, &dtos.NavLink{
Text: "Pipeline", Id: "live-pipeline", Url: hs.Cfg.AppSubURL + "/live/pipeline", Icon: "arrow-to-right",
})
liveNavLinks = append(liveNavLinks, &dtos.NavLink{
Text: "Cloud", Id: "live-cloud", Url: hs.Cfg.AppSubURL + "/live/cloud", Icon: "cloud-upload",
})
navTree = append(navTree, &dtos.NavLink{
Id: "live",
Text: "Live",
SubTitle: "Event Streaming",
Icon: "exchange-alt",
Url: hs.Cfg.AppSubURL + "/live",
Children: liveNavLinks,
HideFromMenu: true,
})
}
if len(configNodes) > 0 {
navTree = append(navTree, &dtos.NavLink{
Id: dtos.NavIDCfg,

View File

@ -0,0 +1,48 @@
import React, { useEffect, useState } from 'react';
import { getBackendSrv } from '@grafana/runtime';
import { useStyles } 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 { GrafanaCloudBackend } from './types';
export default function CloudAdminPage() {
const navModel = useNavModel('live-cloud');
const [cloud, setCloud] = useState<GrafanaCloudBackend[]>([]);
const styles = useStyles(getStyles);
useEffect(() => {
getBackendSrv()
.get(`api/live/remote-write-backends`)
.then((data) => {
setCloud(data.remoteWriteBackends);
})
.catch((e) => console.error(e));
}, []);
return (
<Page navModel={navModel}>
<Page.Contents>
{!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 getStyles = (theme: GrafanaTheme) => {
return {
row: css`
cursor: pointer;
`,
};
};

View File

@ -0,0 +1,21 @@
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>
);
}

View File

@ -0,0 +1,13 @@
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>
);
}

View File

@ -0,0 +1,107 @@
import React, { useEffect, useState, ChangeEvent } from 'react';
import { getBackendSrv } from '@grafana/runtime';
import { Input, Tag, useStyles } 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 { RuleModal } from './RuleModal';
function renderOutputTags(key: string, output?: Output): React.ReactNode {
if (!output?.type) {
return null;
}
if (output.multiple?.outputs?.length) {
return output.multiple?.outputs.map((v, i) => renderOutputTags(`${key}-${i}`, v));
}
return <Tag key={key} name={output.type} />;
}
export default function PipelineAdminPage() {
const [rules, setRules] = useState<Rule[]>([]);
const [isOpen, setOpen] = useState(false);
const [selectedRule, setSelectedRule] = useState<Rule>();
const [defaultRules, setDefaultRules] = useState<any[]>([]);
const navModel = useNavModel('live-pipeline');
const styles = useStyles(getStyles);
useEffect(() => {
getBackendSrv()
.get(`api/live/channel-rules`)
.then((data) => {
setRules(data.rules);
setDefaultRules(data.rules);
})
.catch((e) => console.error(e));
}, []);
const onRowClick = (event: any) => {
const pattern = event.target.getAttribute('data-pattern');
const column = event.target.getAttribute('data-column');
console.log('show:', column);
// setActiveTab(column);
setSelectedRule(rules.filter((rule) => rule.pattern === pattern)[0]);
setOpen(true);
};
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);
}
};
return (
<Page navModel={navModel}>
<Page.Contents>
<div className="page-action-bar">
<div className="gf-form gf-form--grow">
<Input placeholder="Search pattern..." onChange={onSearchQueryChange} />
</div>
</div>
<div className="admin-list-table">
<table className="filter-table filter-table--hover form-inline">
<thead>
<tr>
<th>Pattern</th>
<th>Converter</th>
<th>Processor</th>
<th>Output</th>
</tr>
</thead>
<tbody>
{rules.map((rule) => (
<tr key={rule.pattern} onClick={onRowClick} className={styles.row}>
<td data-pattern={rule.pattern} data-column="pattern">
{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?.processor?.type}
</td>
<td data-pattern={rule.pattern} data-column="output">
{renderOutputTags('out', rule.settings?.output)}
</td>
</tr>
))}
</tbody>
</table>
</div>
{isOpen && selectedRule && <RuleModal rule={selectedRule} isOpen={isOpen} onClose={() => setOpen(false)} />}
</Page.Contents>
</Page>
);
}
const getStyles = (theme: GrafanaTheme) => {
return {
row: css`
cursor: pointer;
`,
};
};

View File

@ -0,0 +1,99 @@
import React, { useState } from 'react';
import { Modal, TabContent, TabsBar, Tab, CodeEditor } from '@grafana/ui';
import { Rule } from './types';
interface Props {
rule: Rule;
isOpen: boolean;
onClose: () => void;
}
const tabs = [
{ 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');
return (
<Modal isOpen={isOpen} title={rule.pattern} onDismiss={onClose} closeOnEscape>
<TabsBar>
{tabs.map((tab, index) => {
return (
<Tab
key={index}
label={tab.label}
active={tab.value === activeTab}
onChangeTab={() => {
setActiveTab(tab.value);
}}
/>
);
})}
</TabsBar>
<TabContent>
{activeTab === 'converter' && <ConverterEditor {...props} />}
{activeTab === 'processor' && <ProcessorEditor {...props} />}
{activeTab === 'output' && <OutputEditor {...props} />}
</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}
/>
);
};

View File

@ -0,0 +1,40 @@
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/helpers';
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')
),
}));
}

View File

@ -0,0 +1,37 @@
export interface Converter {
type: string;
[t: string]: any;
}
export interface Processor {
type: string;
[t: string]: any;
}
export interface Output {
type: string;
[t: string]: any;
multiple?: {
outputs: Output[];
};
}
export interface RuleSettings {
converter?: Converter;
processor?: Processor;
output?: Output;
}
export interface Rule {
pattern: string;
settings: RuleSettings;
}
export interface Pipeline {
rules: Rule[];
}
export interface GrafanaCloudBackend {
uid: string;
settings: any;
}

View File

@ -10,6 +10,7 @@ import { Redirect } from 'react-router-dom';
import ErrorPage from 'app/core/components/ErrorPage/ErrorPage';
import { getPluginsAdminRoutes } from 'app/features/plugins/routes';
import { contextSrv } from 'app/core/services/context_srv';
import { getLiveRoutes } from 'app/features/live/pages/routes';
export const extraRoutes: RouteDescriptor[] = [];
@ -515,6 +516,7 @@ export function getAppRoutes(): RouteDescriptor[] {
),
},
...getPluginsAdminRoutes(),
...getLiveRoutes(),
...extraRoutes,
{
path: '/*',