mirror of
https://github.com/grafana/grafana.git
synced 2025-01-19 13:03:32 -06: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:
parent
d3a7e0228c
commit
45e67630e8
@ -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
|
||||
|
@ -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,
|
||||
|
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 { 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: '/*',
|
||||
|
Loading…
Reference in New Issue
Block a user