Implement tasks 1-3: sidebar order, jail activation rollback, pie chart colors
Task 1: Move Configuration to last position in sidebar NAV_ITEMS
Task 2: Add automatic rollback when jail activation fails
- Back up .local override file before writing
- Restore original file (or delete) on reload failure, health-check
failure, or jail not appearing post-reload
- Return recovered=True/False in JailActivationResponse
- Show warning/critical banner in ActivateJailDialog based on recovery
- Add _restore_local_file_sync and _rollback_activation_async helpers
- Add 3 new tests: rollback on reload failure, health-check failure,
and double failure (recovered=False)
Task 3: Color pie chart legend labels to match their slice color
- legendFormatter now returns ReactNode with span style={{ color }}
- Import LegendPayload from recharts/types/component/DefaultLegendContent
This commit was merged in pull request #1.
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
Tooltip,
|
||||
} from "recharts";
|
||||
import type { PieLabelRenderProps } from "recharts";
|
||||
import type { LegendPayload } from "recharts/types/component/DefaultLegendContent";
|
||||
import type { TooltipContentProps } from "recharts/types/component/Tooltip";
|
||||
import { tokens, makeStyles, Text } from "@fluentui/react-components";
|
||||
import { CHART_PALETTE, resolveFluentToken } from "../utils/chartTheme";
|
||||
@@ -153,12 +154,19 @@ export function TopCountriesPieChart({
|
||||
);
|
||||
}
|
||||
|
||||
/** Format legend entries as "Country Name (xx%)" */
|
||||
const legendFormatter = (value: string): string => {
|
||||
/** Format legend entries as "Country Name (xx%)" and colour them to match their slice. */
|
||||
const legendFormatter = (
|
||||
value: string,
|
||||
entry: LegendPayload,
|
||||
): React.ReactNode => {
|
||||
const slice = slices.find((s) => s.name === value);
|
||||
if (slice == null || total === 0) return value;
|
||||
const pct = ((slice.value / total) * 100).toFixed(1);
|
||||
return `${value} (${pct}%)`;
|
||||
return (
|
||||
<span style={{ color: entry.color }}>
|
||||
{value} ({pct}%)
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
Input,
|
||||
MessageBar,
|
||||
MessageBarBody,
|
||||
MessageBarTitle,
|
||||
Spinner,
|
||||
Text,
|
||||
tokens,
|
||||
@@ -85,6 +86,7 @@ export function ActivateJailDialog({
|
||||
const [logpath, setLogpath] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [recoveryStatus, setRecoveryStatus] = useState<"recovered" | "unrecovered" | null>(null);
|
||||
|
||||
// Pre-activation validation state
|
||||
const [validating, setValidating] = useState(false);
|
||||
@@ -98,6 +100,7 @@ export function ActivateJailDialog({
|
||||
setPort("");
|
||||
setLogpath("");
|
||||
setError(null);
|
||||
setRecoveryStatus(null);
|
||||
setValidationIssues([]);
|
||||
setValidationWarnings([]);
|
||||
};
|
||||
@@ -153,10 +156,17 @@ export function ActivateJailDialog({
|
||||
activateJail(jail.name, overrides)
|
||||
.then((result) => {
|
||||
if (!result.active) {
|
||||
// Backend rejected the activation (e.g. missing logpath or filter).
|
||||
// Show the server's message and keep the dialog open so the user
|
||||
// can read the explanation without the dialog disappearing.
|
||||
setError(result.message);
|
||||
if (result.recovered === true) {
|
||||
// Activation failed but the system rolled back automatically.
|
||||
setRecoveryStatus("recovered");
|
||||
} else if (result.recovered === false) {
|
||||
// Activation failed and rollback also failed.
|
||||
setRecoveryStatus("unrecovered");
|
||||
} else {
|
||||
// Backend rejected before writing (e.g. missing logpath or filter).
|
||||
// Show the server's message and keep the dialog open.
|
||||
setError(result.message);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (result.validation_warnings.length > 0) {
|
||||
@@ -323,6 +333,31 @@ export function ActivateJailDialog({
|
||||
onChange={(_e, d) => { setLogpath(d.value); }}
|
||||
/>
|
||||
</Field>
|
||||
{recoveryStatus === "recovered" && (
|
||||
<MessageBar
|
||||
intent="warning"
|
||||
style={{ marginTop: tokens.spacingVerticalS }}
|
||||
>
|
||||
<MessageBarBody>
|
||||
<MessageBarTitle>Activation Failed — System Recovered</MessageBarTitle>
|
||||
Activation of jail “{jail.name}” failed. The server
|
||||
has been automatically recovered.
|
||||
</MessageBarBody>
|
||||
</MessageBar>
|
||||
)}
|
||||
{recoveryStatus === "unrecovered" && (
|
||||
<MessageBar
|
||||
intent="error"
|
||||
style={{ marginTop: tokens.spacingVerticalS }}
|
||||
>
|
||||
<MessageBarBody>
|
||||
<MessageBarTitle>Activation Failed — Manual Intervention Required</MessageBarTitle>
|
||||
Activation of jail “{jail.name}” failed and
|
||||
automatic recovery was unsuccessful. Manual intervention is
|
||||
required.
|
||||
</MessageBarBody>
|
||||
</MessageBar>
|
||||
)}
|
||||
{error && (
|
||||
<MessageBar
|
||||
intent="error"
|
||||
|
||||
@@ -185,9 +185,9 @@ const NAV_ITEMS: NavItem[] = [
|
||||
{ label: "Dashboard", to: "/", icon: <GridRegular />, end: true },
|
||||
{ label: "World Map", to: "/map", icon: <MapRegular /> },
|
||||
{ label: "Jails", to: "/jails", icon: <ShieldRegular /> },
|
||||
{ label: "Configuration", to: "/config", icon: <SettingsRegular /> },
|
||||
{ label: "History", to: "/history", icon: <HistoryRegular /> },
|
||||
{ label: "Blocklists", to: "/blocklists", icon: <ListRegular /> },
|
||||
{ label: "Configuration", to: "/config", icon: <SettingsRegular /> },
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -553,6 +553,13 @@ export interface JailActivationResponse {
|
||||
fail2ban_running: boolean;
|
||||
/** Non-fatal pre-activation validation warnings (e.g. missing log path). */
|
||||
validation_warnings: string[];
|
||||
/**
|
||||
* Set when activation failed after the config file was already written.
|
||||
* `true` = the system rolled back and recovered automatically.
|
||||
* `false` = rollback also failed — manual intervention required.
|
||||
* `undefined` = activation succeeded or failed before the file was written.
|
||||
*/
|
||||
recovered?: boolean;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user