mirror of https://github.com/grafana/grafana
Live: admin config UI (#39103)
Co-authored-by: Ryan McKinley <ryantxu@gmail.com> Co-authored-by: Atif Ali <atifshoukatali@yahoo.com>pull/39170/head
parent
d3a7e0228c
commit
45e67630e8
@ -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; |
||||
`,
|
||||
}; |
||||
}; |
||||
@ -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> |
||||
); |
||||
} |
||||
@ -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> |
||||
); |
||||
} |
||||
@ -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; |
||||
`,
|
||||
}; |
||||
}; |
||||
@ -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} |
||||
/> |
||||
); |
||||
}; |
||||
@ -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') |
||||
), |
||||
})); |
||||
} |
||||
@ -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; |
||||
} |
||||
Loading…
Reference in new issue