Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add service boot info chart #20085

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
143 changes: 143 additions & 0 deletions pkg/systemd/bootInfo.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import React, { useEffect, useState } from "react";
import cockpit from "cockpit";
import {
Card,
CardBody,
CardTitle, CodeBlock, CodeBlockCode,
EmptyStateVariant,
List,
ListItem,
ListVariant, Spinner
} from "@patternfly/react-core";
import "./bootInfo.scss";
import { EmptyStatePanel } from "../lib/cockpit-components-empty-state.jsx";

const _ = cockpit.gettext;

export function BootInfo({ user }) {
const [svg, setSvg] = useState(undefined);
const [text, setText] = useState(null);
const userMode = user !== "system";

useEffect(() => {
const cmd = userMode ? ["systemd-analyze", "--user", "plot"] : ["systemd-analyze", "plot"];
cockpit.spawn(cmd)
.then(svg_xml => {
try {
const doc = new DOMParser().parseFromString(svg_xml, "text/xml");

const topLevelText = doc.querySelectorAll("*:not(g) > text");
const topLevelTextContent = [...topLevelText].map(e => {
const content = e.textContent;
e.remove();
return content;
});
setText(topLevelTextContent);

const svgElem = doc.querySelector("svg");
svgElem.style.scale = "0.5";
const [plot, legend] = doc.querySelectorAll("g");
legend.remove();
[...plot.querySelectorAll("text.left"), ...plot.querySelectorAll("text.right")].forEach((text) => {
// Sets up attributes to make it possible jump to the page of a specific service
const match = text.innerHTML.match(/^(?<service>.+\.[a-z._-]+)(\s+\((?<time>\d+(\.\d+)?)(?<time_unit>\w+)\))?$/);
// Regex example: "initrd-parse-etc.service (52ms)" -> service: "initrd-parse-etc.service", time: "52", time_unit: "ms"
if (match !== null) {
text.setAttribute("data-service", match.groups.service);
text.setAttribute("data-time", match.groups.time);
text.setAttribute("data-time-unit", match.groups.time_unit);
text.classList.add("clickable-service");
}
});
setSvg(doc.documentElement);
} catch (e) {
setSvg(null);
setText(_("There was an error parsing the output of systemd-analyze"));
}
})
.catch((e) => {
setSvg(null);
setText(_("There was an error reading the output of systemd-analyze"));
});
}, [userMode]);

if (svg === undefined) {
const paragraph = (
<Spinner size="xl" />
);
return (
<div className="pf-v5-c-page__main-section">
<EmptyStatePanel variant={EmptyStateVariant.xs} title={_("Loading")} headingLevel="h4" paragraph={paragraph} />
</div>
);
}

if (svg === null) {
const paragraph = (
<>
{_("systemd-analyze failed to load boot info and returned the following error:")}
</>
);
const secondary = (
<CodeBlock>
<CodeBlockCode id="code-content">{text.toString()}</CodeBlockCode>
</CodeBlock>
);
return (
<div className="pf-v5-c-page__main-section">
<EmptyStatePanel variant={EmptyStateVariant.xs} title={_("Failure")} headingLevel="h4" paragraph={paragraph} secondary={secondary} />
</div>
);
}

const plotClicked = (event) => {
const service = event.target.getAttribute("data-service");
if (service !== null) {
cockpit.jump(`/system/services#/${service}`, cockpit.transport.host);
}
};

return (
<div className="pf-v5-c-page__main-section">
<Card>
<CardTitle>{ _("Boot Info") }</CardTitle>
<CardBody>
<>
{text.map(t => {
return <p key={t}>{t}</p>;
})}
</>
<List className="legend" isPlain variant={ListVariant.inline}>
<ListItem>
<div className="legendColor activating" />
{ _("Activating") }
</ListItem>
<ListItem>
<div className="legendColor active" />
{ _("Active") }
</ListItem>
<ListItem>
<div className="legendColor deactivating" />
{ _("Deactivating") }
</ListItem>
<ListItem>
<div className="legendColor security" />
{ _("Setting up security module") }
</ListItem>
<ListItem>
<div className="legendColor generators" />
{ _("Generators") }
</ListItem>
<ListItem>
<div className="legendColor unitsload" />
{ _("Loading unit files") }
</ListItem>
</List>
<div className="chart-container">
<div className="chart" role="presentation" onClick={plotClicked} onKeyDown={(_) => null} dangerouslySetInnerHTML={{ __html: svg.outerHTML }} />
</div>
</CardBody>
</Card>
</div>
);
}
183 changes: 183 additions & 0 deletions pkg/systemd/bootInfo.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
rect {
stroke-width: 1;
stroke-opacity: 0;
}

rect.background {
fill: rgb(255 255 255);
}

.activating {
fill: var(--pf-v5-global--palette--green-100);

&.legendColor {
background-color: var(--pf-v5-global--palette--green-100);
}
fill-opacity: 0.7;
}

.active {
fill: var(--pf-v5-global--palette--blue-100);

&.legendColor {
background-color: var(--pf-v5-global--palette--blue-100);
}
fill-opacity: 0.7;
}

.deactivating {
fill: var(--pf-v5-global--palette--red-100);

&.legendColor {
background-color: var(--pf-v5-global--palette--red-100);
}
fill-opacity: 0.7;
}

.kernel {
fill: var(--pf-v5-global--palette--black-300);

&.legendColor {
background-color: var(--pf-v5-global--palette--black-300);
}
fill-opacity: 0.7;
}

.initrd {
fill: var(--pf-v5-global--palette--black-300);

&.legendColor {
background-color: var(--pf-v5-global--palette--black-300);
}
fill-opacity: 0.7;
}

.firmware {
fill: var(--pf-v5-global--palette--black-300);

&.legendColor {
background-color: var(--pf-v5-global--palette--black-300);
}
fill-opacity: 0.7;
}

.loader {
fill: var(--pf-v5-global--palette--black-300);

&.legendColor {
background-color: var(--pf-v5-global--palette--black-300);
}
fill-opacity: 0.7;
}

.userspace {
fill: var(--pf-v5-global--palette--black-300);

&.legendColor {
background-color: var(--pf-v5-global--palette--black-300);
}
fill-opacity: 0.7;
}

.security {
fill: var(--pf-v5-global--palette--orange-200);

&.legendColor {
background-color: var(--pf-v5-global--palette--orange-200);
}
fill-opacity: 0.7;
}

.generators {
fill: var(--pf-v5-global--palette--cyan-200);

&.legendColor {
background-color: var(--pf-v5-global--palette--cyan-200);
}
fill-opacity: 0.7;
}

.unitsload {
fill: var(--pf-v5-global--palette--purple-200);

&.legendColor {
background-color: var(--pf-v5-global--palette--purple-200);
}
fill-opacity: 0.7;
}

rect.box {
fill: rgb(240 240 240);
stroke: rgb(192 192 192);
}

line {
stroke: rgb(64 64 64);
stroke-width: 1;
}

line.sec5 {
stroke-width: 2;
}

line.sec01 {
stroke: rgb(224 224 224);
stroke-width: 1;
}

text {
font-family: Verdana, Helvetica, sans-serif;
font-size: 14px;
}

text.left {
font-family: Verdana, Helvetica, sans-serif;
font-size: 14px;
text-anchor: start;
}

text.right {
font-family: Verdana, Helvetica, sans-serif;
font-size: 14px;
text-anchor: end;
}

text.sec {
font-size: 10px;
}

.legendColor {
block-size: 1em;
inline-size: 1em;
margin-inline-end: 0.5em;
vertical-align: middle;
display: inline-block;
}

.legend {
margin-block-start: 1em;
margin-block-end: 1em;
}

.chart-container {
max-inline-size: 100%;
margin-inline: 1em;
max-block-size: 80vh;
overflow: scroll;
}

.chart {
// Crop top where text was removed
margin-block-start: -4em;
font-family: var(--pf-v5-global--FontFamily--text);
}

.clickable-service {
cursor: pointer;
z-index: 10;
font-weight: bold;
}

.clickable-service:hover {
text-decoration: underline;
}
3 changes: 2 additions & 1 deletion pkg/systemd/service-tabs.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ export function ServiceTabs({ onChange, activeTab, tabErrors }) {
target: _("Targets"),
socket: _("Sockets"),
timer: _("Timers"),
path: _("Paths")
path: _("Paths"),
boot: _("Boot")
};

const [activeItem, setActiveItem] = useState(activeTab);
Expand Down
8 changes: 6 additions & 2 deletions pkg/systemd/services.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import { WithDialogs } from "dialogs.jsx";

import s_bus from "./busnames.js";
import "./services.scss";
import { BootInfo } from "./bootInfo.jsx";

const _ = cockpit.gettext;

Expand Down Expand Up @@ -921,15 +922,18 @@ const ServicesPage = () => {
{activeTab == "timer" && owner == "system" && superuser.allowed && <CreateTimerDialog isLoading={isLoading} owner={owner} />}
</Flex>
</PageSection>}
<ServicesPageBody
{ activeTab == "boot"
? <BootInfo user={owner} />
: <ServicesPageBody
key={owner}
activeTab={activeTab}
owner={owner}
privileged={superuser.allowed}
setTabErrors={setTabErrors}
isLoading={isLoading}
setIsLoading={setIsLoading}
/>
/>
}
</Page>
</WithDialogs>
);
Expand Down