Adapt UI for Prometheus Agent (#9851)

* Adapt UI for Prometheus Agent

UI is not my strongest skill, but I'd like to have something minimal for
the initial release of the agent.

Signed-off-by: Julien Pivotto <roidelapluie@inuits.eu>

* Address review comments

Signed-off-by: Julien Pivotto <roidelapluie@inuits.eu>

* Add tests

Signed-off-by: Julien Pivotto <roidelapluie@inuits.eu>

* Add tests, serve only current mode paths

Signed-off-by: Julien Pivotto <roidelapluie@inuits.eu>

* Update js style, add agent test

Signed-off-by: Julien Pivotto <roidelapluie@inuits.eu>
This commit is contained in:
Julien Pivotto 2021-11-30 11:21:07 +01:00 committed by GitHub
parent 0d20ee46fe
commit c78fcd29ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 102 additions and 34 deletions

View File

@ -15,10 +15,11 @@
It will render a "Consoles" link in the navbar when it is non-empty.
- PROMETHEUS_AGENT_MODE is replaced by a boolean indicating if Prometheus is running in agent mode.
It true, it will disable querying capacities in the UI and generally adapt the UI to the agent mode.
It has to be represented as a string, because booleans can be mangled to !1 in production builds.
-->
<script>
const GLOBAL_CONSOLES_LINK='CONSOLES_LINK_PLACEHOLDER';
const GLOBAL_PROMETHEUS_AGENT_MODE=PROMETHEUS_AGENT_MODE_PLACEHOLDER;
const GLOBAL_AGENT_MODE='AGENT_MODE_PLACEHOLDER';
</script>
<!--

View File

@ -5,6 +5,7 @@ import Navigation from './Navbar';
import { Container } from 'reactstrap';
import { Route } from 'react-router-dom';
import {
AgentPage,
AlertsPage,
ConfigPage,
FlagsPage,
@ -24,6 +25,7 @@ describe('App', () => {
});
it('routes', () => {
[
AgentPage,
AlertsPage,
ConfigPage,
FlagsPage,
@ -37,7 +39,7 @@ describe('App', () => {
const c = app.find(component);
expect(c).toHaveLength(1);
});
expect(app.find(Route)).toHaveLength(9);
expect(app.find(Route)).toHaveLength(10);
expect(app.find(Container)).toHaveLength(1);
});
});

View File

@ -4,6 +4,7 @@ import { Container } from 'reactstrap';
import { BrowserRouter as Router, Redirect, Switch, Route } from 'react-router-dom';
import {
AgentPage,
AlertsPage,
ConfigPage,
FlagsPage,
@ -22,14 +23,16 @@ import useMedia from './hooks/useMedia';
interface AppProps {
consolesLink: string | null;
agentMode: boolean;
}
const App: FC<AppProps> = ({ consolesLink }) => {
const App: FC<AppProps> = ({ consolesLink, agentMode }) => {
// This dynamically/generically determines the pathPrefix by stripping the first known
// endpoint suffix from the window location path. It works out of the box for both direct
// hosting and reverse proxy deployments with no additional configurations required.
let basePath = window.location.pathname;
const paths = [
'/agent',
'/graph',
'/alerts',
'/status',
@ -70,14 +73,17 @@ const App: FC<AppProps> = ({ consolesLink }) => {
<Theme />
<PathPrefixContext.Provider value={basePath}>
<Router basename={basePath}>
<Navigation consolesLink={consolesLink} />
<Navigation consolesLink={consolesLink} agentMode={agentMode} />
<Container fluid style={{ paddingTop: 70 }}>
<Switch>
<Redirect exact from="/" to={`graph`} />
<Redirect exact from="/" to={agentMode ? '/agent' : '/graph'} />
{/*
NOTE: Any route added here needs to also be added to the list of
React-handled router paths ("reactRouterPaths") in /web/web.go.
*/}
<Route path="/agent">
<AgentPage />
</Route>
<Route path="/graph">
<PanelListPage />
</Route>

View File

@ -17,17 +17,18 @@ import { ThemeToggle } from './Theme';
interface NavbarProps {
consolesLink: string | null;
agentMode: boolean;
}
const Navigation: FC<NavbarProps> = ({ consolesLink }) => {
const Navigation: FC<NavbarProps> = ({ consolesLink, agentMode }) => {
const [isOpen, setIsOpen] = useState(false);
const toggle = () => setIsOpen(!isOpen);
const pathPrefix = usePathPrefix();
return (
<Navbar className="mb-3" dark color="dark" expand="md" fixed="top">
<NavbarToggler onClick={toggle} className="mr-2" />
<Link className="pt-0 navbar-brand" to="/graph">
Prometheus
<Link className="pt-0 navbar-brand" to={agentMode ? '/agent' : '/graph'}>
Prometheus{agentMode && ' Agent'}
</Link>
<Collapse isOpen={isOpen} navbar style={{ justifyContent: 'space-between' }}>
<Nav className="ml-0" navbar>
@ -36,16 +37,20 @@ const Navigation: FC<NavbarProps> = ({ consolesLink }) => {
<NavLink href={consolesLink}>Consoles</NavLink>
</NavItem>
)}
<NavItem>
<NavLink tag={Link} to="/alerts">
Alerts
</NavLink>
</NavItem>
<NavItem>
<NavLink tag={Link} to="/graph">
Graph
</NavLink>
</NavItem>
{!agentMode && (
<>
<NavItem>
<NavLink tag={Link} to="/alerts">
Alerts
</NavLink>
</NavItem>
<NavItem>
<NavLink tag={Link} to="/graph">
Graph
</NavLink>
</NavItem>
</>
)}
<UncontrolledDropdown nav inNavbar>
<DropdownToggle nav caret>
Status
@ -54,18 +59,22 @@ const Navigation: FC<NavbarProps> = ({ consolesLink }) => {
<DropdownItem tag={Link} to="/status">
Runtime & Build Information
</DropdownItem>
<DropdownItem tag={Link} to="/tsdb-status">
TSDB Status
</DropdownItem>
{!agentMode && (
<DropdownItem tag={Link} to="/tsdb-status">
TSDB Status
</DropdownItem>
)}
<DropdownItem tag={Link} to="/flags">
Command-Line Flags
</DropdownItem>
<DropdownItem tag={Link} to="/config">
Configuration
</DropdownItem>
<DropdownItem tag={Link} to="/rules">
Rules
</DropdownItem>
{!agentMode && (
<DropdownItem tag={Link} to="/rules">
Rules
</DropdownItem>
)}
<DropdownItem tag={Link} to="/targets">
Targets
</DropdownItem>
@ -77,9 +86,11 @@ const Navigation: FC<NavbarProps> = ({ consolesLink }) => {
<NavItem>
<NavLink href="https://prometheus.io/docs/prometheus/latest/getting_started/">Help</NavLink>
</NavItem>
<NavItem>
<NavLink href={`${pathPrefix}/classic/graph${window.location.search}`}>Classic UI</NavLink>
</NavItem>
{!agentMode && (
<NavItem>
<NavLink href={`${pathPrefix}/classic/graph${window.location.search}`}>Classic UI</NavLink>
</NavItem>
)}
</Nav>
</Collapse>
<ThemeToggle />

View File

@ -10,8 +10,10 @@ import { isPresent } from './utils';
// Declared/defined in public/index.html, value replaced by Prometheus when serving bundle.
declare const GLOBAL_CONSOLES_LINK: string;
declare const GLOBAL_AGENT_MODE: string;
let consolesLink: string | null = GLOBAL_CONSOLES_LINK;
const agentMode: string | null = GLOBAL_AGENT_MODE;
if (
GLOBAL_CONSOLES_LINK === 'CONSOLES_LINK_PLACEHOLDER' ||
@ -21,4 +23,4 @@ if (
consolesLink = null;
}
ReactDOM.render(<App consolesLink={consolesLink} />, document.getElementById('root'));
ReactDOM.render(<App consolesLink={consolesLink} agentMode={agentMode === 'true'} />, document.getElementById('root'));

View File

@ -0,0 +1,16 @@
import React, { FC } from 'react';
const Agent: FC = () => {
return (
<>
<h2>Prometheus Agent</h2>
<p>
This Prometheus instance is running in <strong>agent mode</strong>. In this mode, Prometheus is only used to scrape
discovered targets and forward them to remote write endpoints.
</p>
<p>Some features are not available in this mode, such as querying and alerting.</p>
</>
);
};
export default Agent;

View File

@ -1,3 +1,4 @@
import Agent from './agent/Agent';
import Alerts from './alerts/Alerts';
import Config from './config/Config';
import Flags from './flags/Flags';
@ -9,6 +10,7 @@ import PanelList from './graph/PanelList';
import TSDBStatus from './tsdbStatus/TSDBStatus';
import { withStartingIndicator } from '../components/withStartingIndicator';
const AgentPage = withStartingIndicator(Agent);
const AlertsPage = withStartingIndicator(Alerts);
const ConfigPage = withStartingIndicator(Config);
const FlagsPage = withStartingIndicator(Flags);
@ -21,6 +23,7 @@ const PanelListPage = withStartingIndicator(PanelList);
// prettier-ignore
export {
AgentPage,
AlertsPage,
ConfigPage,
FlagsPage,

View File

@ -71,18 +71,27 @@ import (
// Paths that are handled by the React / Reach router that should all be served the main React app's index.html.
var reactRouterPaths = []string{
"/alerts",
"/config",
"/flags",
"/graph",
"/rules",
"/service-discovery",
"/status",
"/targets",
"/tsdb-status",
"/starting",
}
// Paths that are handled by the React router when the Agent mode is set.
var reactRouterAgentPaths = []string{
"/agent",
}
// Paths that are handled by the React router when the Agent mode is not set.
var reactRouterServerPaths = []string{
"/alerts",
"/graph",
"/rules",
"/tsdb-status",
}
// withStackTrace logs the stack trace in case the request panics. The function
// will re-raise the error which will then be handled by the net/http package.
// It is needed because the go-kit log package doesn't manage properly the
@ -346,10 +355,15 @@ func New(logger log.Logger, o *Options) *Handler {
router = router.WithPrefix(o.RoutePrefix)
}
homePage := "/graph"
if o.IsAgent {
homePage = "/agent"
}
readyf := h.testReady
router.Get("/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, path.Join(o.ExternalURL.Path, "/graph"), http.StatusFound)
http.Redirect(w, r, path.Join(o.ExternalURL.Path, homePage), http.StatusFound)
})
router.Get("/classic/", func(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, path.Join(o.ExternalURL.Path, "/classic/graph"), http.StatusFound)
@ -409,7 +423,7 @@ func New(logger log.Logger, o *Options) *Handler {
}
replacedIdx := bytes.ReplaceAll(idx, []byte("CONSOLES_LINK_PLACEHOLDER"), []byte(h.consolesPath()))
replacedIdx = bytes.ReplaceAll(replacedIdx, []byte("TITLE_PLACEHOLDER"), []byte(h.options.PageTitle))
replacedIdx = bytes.ReplaceAll(replacedIdx, []byte("PROMETHEUS_AGENT_MODE_PLACEHOLDER"), []byte(strconv.FormatBool(h.options.IsAgent)))
replacedIdx = bytes.ReplaceAll(replacedIdx, []byte("AGENT_MODE_PLACEHOLDER"), []byte(strconv.FormatBool(h.options.IsAgent)))
w.Write(replacedIdx)
}
@ -418,6 +432,16 @@ func New(logger log.Logger, o *Options) *Handler {
router.Get(p, serveReactApp)
}
if h.options.IsAgent {
for _, p := range reactRouterAgentPaths {
router.Get(p, serveReactApp)
}
} else {
for _, p := range reactRouterServerPaths {
router.Get(p, serveReactApp)
}
}
// The favicon and manifest are bundled as part of the React app, but we want to serve
// them on the root.
for _, p := range []string{"/favicon.ico", "/manifest.json"} {

View File

@ -585,6 +585,8 @@ func TestAgentAPIEndPoints(t *testing.T) {
"/query",
"/query_range",
"/query_exemplars",
"/graph",
"/rules",
} {
w := httptest.NewRecorder()
req, err := http.NewRequest("GET", baseURL+u, nil)
@ -595,6 +597,7 @@ func TestAgentAPIEndPoints(t *testing.T) {
// Test for available endpoints in the Agent mode.
for _, u := range []string{
"/agent",
"/targets",
"/status",
} {