mirror of
https://github.com/grafana/grafana.git
synced 2025-08-01 12:23:10 +08:00
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:
@ -72,9 +72,12 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, hs.Index)
|
r.Get("/admin/orgs/edit/:id", reqGrafanaAdmin, hs.Index)
|
||||||
r.Get("/admin/stats", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionServerStatsRead)), 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("/admin/ldap", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPStatusRead)), hs.Index)
|
||||||
|
|
||||||
r.Get("/styleguide", reqSignedIn, 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", reqSignedIn, hs.Index)
|
||||||
r.Get("/plugins/:id/", reqSignedIn, hs.Index)
|
r.Get("/plugins/:id/", reqSignedIn, hs.Index)
|
||||||
r.Get("/plugins/:id/edit", reqSignedIn, hs.Index) // deprecated
|
r.Get("/plugins/:id/edit", reqSignedIn, hs.Index) // deprecated
|
||||||
|
@ -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 {
|
if len(configNodes) > 0 {
|
||||||
navTree = append(navTree, &dtos.NavLink{
|
navTree = append(navTree, &dtos.NavLink{
|
||||||
Id: dtos.NavIDCfg,
|
Id: dtos.NavIDCfg,
|
||||||
|
48
public/app/features/live/pages/CloudAdminPage.tsx
Normal file
48
public/app/features/live/pages/CloudAdminPage.tsx
Normal 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;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
21
public/app/features/live/pages/FeatureTogglePage.tsx
Normal file
21
public/app/features/live/pages/FeatureTogglePage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
13
public/app/features/live/pages/LiveStatusPage.tsx
Normal file
13
public/app/features/live/pages/LiveStatusPage.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
107
public/app/features/live/pages/PipelineAdminPage.tsx
Normal file
107
public/app/features/live/pages/PipelineAdminPage.tsx
Normal 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;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
};
|
99
public/app/features/live/pages/RuleModal.tsx
Normal file
99
public/app/features/live/pages/RuleModal.tsx
Normal 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}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
40
public/app/features/live/pages/routes.ts
Normal file
40
public/app/features/live/pages/routes.ts
Normal 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')
|
||||||
|
),
|
||||||
|
}));
|
||||||
|
}
|
37
public/app/features/live/pages/types.ts
Normal file
37
public/app/features/live/pages/types.ts
Normal 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;
|
||||||
|
}
|
@ -10,6 +10,7 @@ import { Redirect } from 'react-router-dom';
|
|||||||
import ErrorPage from 'app/core/components/ErrorPage/ErrorPage';
|
import ErrorPage from 'app/core/components/ErrorPage/ErrorPage';
|
||||||
import { getPluginsAdminRoutes } from 'app/features/plugins/routes';
|
import { getPluginsAdminRoutes } from 'app/features/plugins/routes';
|
||||||
import { contextSrv } from 'app/core/services/context_srv';
|
import { contextSrv } from 'app/core/services/context_srv';
|
||||||
|
import { getLiveRoutes } from 'app/features/live/pages/routes';
|
||||||
|
|
||||||
export const extraRoutes: RouteDescriptor[] = [];
|
export const extraRoutes: RouteDescriptor[] = [];
|
||||||
|
|
||||||
@ -515,6 +516,7 @@ export function getAppRoutes(): RouteDescriptor[] {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
...getPluginsAdminRoutes(),
|
...getPluginsAdminRoutes(),
|
||||||
|
...getLiveRoutes(),
|
||||||
...extraRoutes,
|
...extraRoutes,
|
||||||
{
|
{
|
||||||
path: '/*',
|
path: '/*',
|
||||||
|
Reference in New Issue
Block a user