Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/99-changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@ identified with a specific icon:
- 🩹: bug fix
- 🌱: miscellaneous change

## FlowScope (unreleased)

- 🌱 *console*: on a small screen, the graph of the visualize page comes
first and the options open in a sheet
- 🌱 *console*: units are written in words, the axis of a graph shows the
unit, times use a 24-hour clock and the first ten series each get a color
of their own
- 🌱 *console*: the home page shows the traffic now and its peak, ranks the
top lists with the share of each entry, and groups the fields of the last
flow. The fields without a value are behind a button
- 🌱 *console*: the user and the theme switch are at the foot of the menu.
The bar above the pages is only shown on a small screen, where it opens
the menu
- 🩹 *console*: the page for an unknown address says what happened and links
to the home page and to the documentation

## FlowScope 2.0.0-rs.0 - 2026-10-10

The first pre-release of *FlowScope*. It is Akvorado, as of its 2026.10.0 version
Expand Down
2 changes: 1 addition & 1 deletion web/src/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
--color-info: var(--info);
--color-info-bg: var(--info-bg);
--shadow-card: var(--shadow-card);
--text-2xs: 0.6875rem;
--text-2xs: 0.75rem;
--text-2xs--line-height: 1rem;
--text-ui: 0.8125rem;
--text-ui--line-height: 1.25rem;
Expand Down
28 changes: 27 additions & 1 deletion web/src/app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,34 @@
// SPDX-FileCopyrightText: 2022 Free Mobile
// SPDX-License-Identifier: AGPL-3.0-only

import type { Metadata } from "next";
import Link from "next/link";
import { ErrorPage } from "@/components/error-page";
import { buttonVariants } from "@/components/ui/button";

export const metadata: Metadata = {
title: "FlowScope | Not found",
};

export default function NotFound() {
return <ErrorPage code="Error 404" error="Not found!" />;
return (
<ErrorPage
code="Error 404"
error="This page does not exist"
description="The address may be mistyped, or the page has moved."
>
<Link href="/" className={buttonVariants({ className: "h-11 px-5" })}>
Back to home
</Link>
<Link
href="/docs"
className={buttonVariants({
variant: "outline",
className: "h-11 bg-card px-5",
})}
>
Open the documentation
</Link>
</ErrorPage>
);
}
48 changes: 32 additions & 16 deletions web/src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,39 +14,55 @@ import {
WidgetGraph,
WidgetLastFlow,
WidgetTop,
WidgetTrafficNow,
WidgetTrafficPeak,
isShareWidget,
} from "@/components/home/widgets";

export default function HomePage() {
const config = useServerConfig();
const topWidgets = config?.homepageTopWidgets ?? [];
const ranked = topWidgets.filter((widget) => !isShareWidget(widget));
const shares = topWidgets.filter(isShareWidget);
return (
<div className="flex w-full flex-col gap-6 px-4 py-6 sm:px-6">
<PageHeader
eyebrow="Overview"
title="Your network traffic at a glance"
description="FlowScope receives flows, adds data like interface names and countries, and stores them in ClickHouse."
actions={
<Link href="/visualize" className={buttonVariants()}>
<Link
href="/visualize"
className={buttonVariants({ className: "h-11 px-5" })}
>
<ChartArea />
Explore flows
</Link>
}
/>
<div className="grid grid-cols-1 gap-4 xl:grid-cols-3">
<div className="grid auto-rows-min grid-cols-2 gap-4 md:grid-cols-4 xl:col-span-2">
<div className="col-span-2 grid grid-cols-2 gap-4 md:col-span-4 md:grid-cols-4">
<WidgetFlowRate />
<WidgetExporters />
</div>
{/* The last row grows to fill the width, whatever their number. */}
<div className="col-span-2 flex flex-wrap gap-4 md:col-span-4">
{topWidgets.map((widget) => (
<div key={widget} className="min-w-36 flex-1 basis-36">
<WidgetTop what={widget} />
</div>
))}
</div>
<WidgetGraph className="col-span-2 md:col-span-4" />
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
<WidgetFlowRate />
<WidgetExporters />
<WidgetTrafficNow />
<WidgetTrafficPeak />
</div>
<div className="grid grid-cols-1 items-start gap-4 xl:grid-cols-3">
<div className="flex min-w-0 flex-col gap-4 xl:col-span-2">
<WidgetGraph />
{/* Each row fills the width, whatever the number of widgets. */}
{[ranked, shares].map(
(widgets, row) =>
widgets.length > 0 && (
<div
key={row}
className="grid grid-cols-[repeat(auto-fit,minmax(13.5rem,1fr))] gap-4"
>
{widgets.map((widget) => (
<WidgetTop key={widget} what={widget} />
))}
</div>
),
)}
</div>
<WidgetLastFlow />
</div>
Expand Down
15 changes: 11 additions & 4 deletions web/src/app/visualize/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ export default function VisualizePage() {
const [error, setError] = useState("");
const [fetching, setFetching] = useState(false);
const [highlight, setHighlight] = useState<number | null>(null);
const [graphHeight, setGraphHeight] = useState(500);
// Unset until the handle is dragged: the height then follows the screen.
const [graphHeight, setGraphHeight] = useState<number | null>(null);
const graph = useRef<HTMLDivElement>(null);
// The last change comes from the auto refresh.
const auto = useRef(false);
const [quiet, setQuiet] = useState(false);
Expand Down Expand Up @@ -199,7 +201,7 @@ export default function VisualizePage() {
// Resize the graph by dragging the handle below it.
const startResize = (event: React.PointerEvent) => {
const startY = event.clientY;
const startHeight = graphHeight;
const startHeight = graph.current?.offsetHeight ?? 0;
const move = (e: PointerEvent) =>
setGraphHeight(Math.max(200, startHeight + e.clientY - startY));
const stop = () => {
Expand All @@ -221,9 +223,10 @@ export default function VisualizePage() {
onCancel={cancel}
/>
)}
<div className="min-w-0 grow overflow-y-auto">
<div className="min-h-0 min-w-0 grow overflow-y-auto">
<div className="flex flex-col gap-5 px-4 py-6 sm:px-6">
<PageHeader
dense
eyebrow="Visualize"
title="Explore your flows"
description="Pick a time range, dimensions and a filter. Select a part of the graph with the mouse to zoom in."
Expand All @@ -237,7 +240,11 @@ export default function VisualizePage() {
</Alert>
)}
<Panel className="relative break-inside-avoid-page" contentClassName="gap-0">
<div style={{ height: graphHeight }}>
<div
ref={graph}
className="h-80 lg:h-[500px]"
style={graphHeight === null ? undefined : { height: graphHeight }}
>
<DataGraph
data={result}
highlight={highlight}
Expand Down
20 changes: 17 additions & 3 deletions web/src/components/brand.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,14 @@ export function PageHeader({
title,
description,
actions,
dense,
}: {
eyebrow: ReactNode;
title: ReactNode;
description?: ReactNode;
actions?: ReactNode;
/** Keep the description for a screen with room for it. */
dense?: boolean;
}) {
return (
<div className="flex flex-wrap items-end justify-between gap-x-6 gap-y-3">
Expand All @@ -55,7 +58,12 @@ export function PageHeader({
{title}
</h1>
{description && (
<p className="max-w-measure text-sm text-muted-foreground">
<p
className={cn(
"max-w-measure text-sm text-muted-foreground",
dense && "hidden sm:block",
)}
>
{description}
</p>
)}
Expand All @@ -74,6 +82,7 @@ export function Panel({
description,
action,
children,
size,
className,
contentClassName,
}: {
Expand All @@ -82,15 +91,20 @@ export function Panel({
description?: ReactNode;
action?: ReactNode;
children: ReactNode;
size?: "default" | "sm";
className?: string;
contentClassName?: string;
}) {
return (
<Card className={className}>
<Card size={size} className={className}>
{(eyebrow || title || action) && (
<CardHeader className="gap-2">
{eyebrow && <SectionLabel>{eyebrow}</SectionLabel>}
{title && <CardTitle>{title}</CardTitle>}
{title && (
<CardTitle>
<h2>{title}</h2>
</CardTitle>
)}
{description && <CardDescription>{description}</CardDescription>}
{action && (
<CardAction className="flex flex-wrap items-center justify-end gap-2">
Expand Down
28 changes: 25 additions & 3 deletions web/src/components/error-page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,37 @@
// SPDX-FileCopyrightText: 2022 Free Mobile
// SPDX-License-Identifier: AGPL-3.0-only

import type { ReactNode } from "react";
import { Logo } from "@/components/logo";
import { SectionLabel } from "@/components/brand";

export function ErrorPage({ code, error }: { code: string; error: string }) {
/** A page for an error, with what happened and the ways out as children. */
export function ErrorPage({
code,
error,
description,
children,
}: {
code: string;
error: string;
description?: string;
children?: ReactNode;
}) {
return (
<div className="flex w-full flex-col items-center justify-center gap-5 p-6 text-center">
<Logo size={120} className="motion-safe:animate-wiggle" />
<Logo size={96} className="motion-safe:animate-wiggle" />
<SectionLabel>{code}</SectionLabel>
<h1 className="text-display font-semibold tracking-display">{error}</h1>
<h1 className="text-title font-semibold leading-tight tracking-display sm:text-display">
{error}
</h1>
{description && (
<p className="max-w-[46ch] text-muted-foreground">{description}</p>
)}
{children && (
<div className="mt-1 flex flex-wrap justify-center gap-3">
{children}
</div>
)}
</div>
);
}
Loading
Loading