Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions .github/workflows/frontend-ui-audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ permissions:
jobs:
audit:
name: Audit registry usage
timeout-minutes: 5
# Advisory, and that has to hold for infrastructure too: a PR that moves a lot of registry files
# (a data-table resync, say) gives the audit far more work, and a job that overruns its timeout
# reports as a failed check no matter how forgiving the steps below are. Hence the generous
# budget here, a tighter cap on the audit itself, and continue-on-error at both levels.
timeout-minutes: 15
continue-on-error: true
runs-on: blacksmith-2vcpu-ubuntu-2404
steps:
- name: Checkout console
Expand Down Expand Up @@ -54,11 +59,11 @@ jobs:
ref: ${{ env.UI_REGISTRY_REF }}
# The audit resolves component versions via `git show v<tag>:<path>`,
# so it needs full history and all v* tags (fetch-depth: 0).
# filter: blob:none makes it a blobless partial clone — commit and tag
# metadata is fetched, but file blobs are pulled lazily only when
# `git show` touches them, keeping the checkout fast.
#
# Deliberately NOT a blobless clone: `git show` per component per tag is exactly the
# access pattern that defeats filter=blob:none, turning each file read into its own
# lazy fetch. The whole repo is ~75MB in one transfer, against hundreds of round trips.
fetch-depth: 0
filter: blob:none
path: ui-registry
token: ${{ env.ACTIONS_BOT_TOKEN }}
- name: Setup Bun
Expand All @@ -72,6 +77,10 @@ jobs:
bun run registry:build
- name: Run audit (markdown report + exit code)
id: audit
# Capped below the job budget so a slow or hung audit is contained here, leaving the
# steps after it free to report what did come back.
timeout-minutes: 8
continue-on-error: true
run: |
set +e
# Exit code only feeds the warning step below; the job never fails on findings.
Expand All @@ -83,11 +92,18 @@ jobs:
> /tmp/audit-body.md
echo "exit_code=$?" >> "$GITHUB_OUTPUT"
- name: Post or update sticky PR comment
if: always()
continue-on-error: true
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
set -euo pipefail
# The audit can be cut short by its own cap, leaving no report — say so rather than
# letting `cat` of a missing file take the job down with it.
if [ ! -s /tmp/audit-body.md ]; then
echo "_UI audit did not finish within its time budget — no findings reported._" > /tmp/audit-body.md
fi
marker='<!-- ui-audit-frontend -->'
{ echo "$marker"; cat /tmp/audit-body.md; } > /tmp/body.md
existing=$(gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \
Expand Down
11 changes: 10 additions & 1 deletion frontend/bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@
"@connectrpc/connect-web": "^2.1.0",
"@dagrejs/dagre": "^3.0.0",
"@emotion/css": "^11.13.5",
"@fontsource/geist-mono": "^5.3.0",
"@fontsource/inter": "^5.3.0",
"@hello-pangea/dnd": "^18.0.1",
"@hookform/resolvers": "^5.2.2",
"@icons-pack/react-simple-icons": "^13.8.0",
Expand Down Expand Up @@ -95,6 +97,7 @@
"es-cookie": "^1.5.0",
"hast": "^1.0.0",
"hast-util-to-jsx-runtime": "^2.3.6",
"inter-ui": "^4.1.1",
"js-base64": "^3.7.8",
"json-bigint": "^1.0.0",
"lottie-react": "^2.4.1",
Expand Down
50 changes: 28 additions & 22 deletions frontend/src/components/layout/header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
* by the Apache License, Version 2.0
*/

import { Button, ColorModeSwitch, CopyButton } from '@redpanda-data/ui';
import { ColorModeSwitch } from '@redpanda-data/ui';
import { Link, useLocation, useMatchRoute, useRouter } from '@tanstack/react-router';
import { cn } from 'components/redpanda-ui/lib/utils';
import { ChevronLeft } from 'lucide-react';
Expand All @@ -28,8 +28,10 @@ import {
BreadcrumbSeparator,
} from '../redpanda-ui/components/breadcrumb';
import { Button as RegistryButton } from '../redpanda-ui/components/button';
import { CopyButton } from '../redpanda-ui/components/copy-button';
import { Separator } from '../redpanda-ui/components/separator';
import { SidebarTrigger } from '../redpanda-ui/components/sidebar';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../redpanda-ui/components/tooltip';

type BreadcrumbHeaderRowProps = {
useNewSidebar: boolean;
Expand Down Expand Up @@ -97,9 +99,8 @@ function AppPageHeader() {
return null;
}

// Embedded, the breadcrumb row holds nothing — the host draws the breadcrumb and there
// is no sidebar trigger — so with the title row hidden this would be a bare divider
// above a page that already has its own title bar.
// Embedded, the breadcrumb row holds nothing (the host draws the breadcrumb, and there is
// no sidebar trigger), so without the title row the header is a bare divider.
if (hideTitleRow && isEmbedded()) {
return null;
}
Expand Down Expand Up @@ -143,21 +144,27 @@ function AppPageHeader() {
</div>
</div>
<div className="flex items-center gap-2">
{!isEmbedded() && api.isRedpanda && (
<Link to="/debug-bundle">
<Button
isDisabled={!api.userData?.canViewDebugBundle}
tooltip={
api.userData?.canViewDebugBundle
? null
: 'You need RedpandaCapability.MANAGE_DEBUG_BUNDLE permission'
}
variant="ghost"
>
Debug bundle
</Button>
</Link>
)}
{!isEmbedded() &&
api.isRedpanda &&
(api.userData?.canViewDebugBundle ? (
<RegistryButton render={<Link to="/debug-bundle">Debug bundle</Link>} variant="ghost" />
) : (
<TooltipProvider>
<Tooltip>
{/* span wrapper: a disabled button swallows pointer events, so it can't anchor the tooltip itself */}
<TooltipTrigger
render={
<span className="inline-flex">
<RegistryButton disabled variant="ghost">
Debug bundle
</RegistryButton>
</span>
}
/>
<TooltipContent>You need RedpandaCapability.MANAGE_DEBUG_BUNDLE permission</TooltipContent>
</Tooltip>
</TooltipProvider>
))}
{IsDev && !isEmbedded() && <ColorModeSwitch m={0} p={0} variant="ghost" />}
</div>
</div>
Expand All @@ -169,11 +176,10 @@ function AppPageHeader() {
export default AppPageHeader;

/**
* Whether the matched route draws its own title bar (`staticData.breadcrumbOnlyHeader`),
* so the header shows only the breadcrumb row.
* Whether the matched route draws its own title bar (`staticData.breadcrumbOnlyHeader`).
*
* Resolved from the pathname rather than `useMatches()`: committed matches lag the
* location by a render on soft navigation, which would flash the title row on the way in.
* location by a render on soft navigation, flashing the title row on the way in.
*/
function useRouteOwnsTitleRow() {
const router = useRouter();
Expand Down
125 changes: 62 additions & 63 deletions frontend/src/components/misc/buttons/data-refresh/component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,10 @@
* by the Apache License, Version 2.0
*/

import { Box, Flex, IconButton, Popover, Spinner, Text } from '@redpanda-data/ui';
import { PauseIcon, PlayIcon, RefreshIcon } from 'components/icons';
import { Button } from 'components/redpanda-ui/components/button';
import { Spinner } from 'components/redpanda-ui/components/spinner';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from 'components/redpanda-ui/components/tooltip';
import { Pause, Play, RefreshCw } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';

import { appGlobal } from '../../../../state/app-global';
Expand Down Expand Up @@ -43,19 +45,15 @@ export const DataRefreshButton = () => {

let newRemainingSeconds = 0;
if (stateRef.current.isActive && currentRequests === 0) {
if (currentRequests > 0) {
// Active requests — delay the next refresh
stateRef.current.nextRefresh = Date.now() + AUTO_REFRESH_INTERVAL_SECS * 1000;
const timeUntilRefresh = stateRef.current.nextRefresh - Date.now();
if (timeUntilRefresh > 0) {
newRemainingSeconds = Math.ceil(timeUntilRefresh / 1000);
} else {
const timeUntilRefresh = stateRef.current.nextRefresh - Date.now();
if (timeUntilRefresh > 0) {
newRemainingSeconds = Math.ceil(timeUntilRefresh / 1000);
} else {
stateRef.current.nextRefresh = Date.now() + AUTO_REFRESH_INTERVAL_SECS * 1000;
appGlobal.onRefresh();
}
stateRef.current.nextRefresh = Date.now() + AUTO_REFRESH_INTERVAL_SECS * 1000;
appGlobal.onRefresh();
}
} else if (stateRef.current.isActive && currentRequests > 0) {
// Active requests — delay the next refresh
stateRef.current.nextRefresh = Date.now() + AUTO_REFRESH_INTERVAL_SECS * 1000;
}

Expand Down Expand Up @@ -83,62 +81,63 @@ export const DataRefreshButton = () => {
const countStr = maxRequestCount > 1 ? `${maxRequestCount - activeRequests} / ${maxRequestCount}` : '';

return (
<div className="flex items-center gap-1">
<Box>
<Popover
content={
<div>
Enable or disable automatic refresh every <span className="codeBox">{AUTO_REFRESH_INTERVAL_SECS}s</span>.
</div>
}
hideCloseButton={true}
isInPortal
placement="bottom"
title="Auto Refresh"
>
<IconButton
aria-label="Auth Refresh"
icon={isActive ? <PauseIcon size={18} /> : <PlayIcon size={18} />}
onClick={toggleAutorefresh}
p={0}
size="xs"
variant="ghost"
<TooltipProvider>
<div className="flex items-center gap-1">
<Tooltip>
<TooltipTrigger
render={
<Button
aria-label={isActive ? 'Pause auto refresh' : 'Start auto refresh'}
className="size-7"
onClick={toggleAutorefresh}
size="icon"
variant="ghost"
>
{isActive ? <Pause /> : <Play />}
</Button>
}
/>
</Popover>
</Box>
<Flex alignItems="center" flexDirection="column">
<TooltipContent className="max-w-56">
<div className="flex flex-col gap-1">
<span className="font-medium">Auto refresh</span>
<span>Automatically refresh the data on this page every {AUTO_REFRESH_INTERVAL_SECS}s.</span>
</div>
</TooltipContent>
</Tooltip>
{isActive || activeRequests > 0 ? (
<Spinner color="red.500" ml={2} size="sm" speed="0.3s" />
<Spinner className="ml-1" />
) : (
<Popover
content={
<div>
Click to force a refresh of the data shown in the current page. When switching pages, any data older
than <span className="codeBox">{prettyMilliseconds(REST_CACHE_DURATION_SEC * 1000)}</span> will be
refreshed automatically.
</div>
}
hideCloseButton={true}
isInPortal
placement="bottom"
title="Force Refresh"
>
<IconButton
aria-label="Force Refresh"
icon={<RefreshIcon size={18} />}
onClick={() => appGlobal.onRefresh()}
p={0}
size="xs"
variant="ghost"
<Tooltip>
<TooltipTrigger
render={
<Button
aria-label="Force refresh"
className="size-7"
onClick={() => appGlobal.onRefresh()}
size="icon"
variant="ghost"
>
<RefreshCw />
</Button>
}
/>
</Popover>
<TooltipContent className="max-w-64">
<div className="flex flex-col gap-1">
<span className="font-medium">Force refresh</span>
<span>
Refresh the data shown on this page. When switching pages, any data older than{' '}
{prettyMilliseconds(REST_CACHE_DURATION_SEC * 1000)} is refreshed automatically.
</span>
</div>
</TooltipContent>
</Tooltip>
)}
</Flex>
<Text fontSize="sm" ml={4} userSelect="none">
{isActive && activeRequests === 0 && <>Refreshing in {remainingSeconds} secs</>}
{activeRequests > 0 && <>Fetching data... {countStr}</>}
</Text>
</div>
<span className="ml-3 select-none text-muted-foreground text-sm">
{isActive && activeRequests === 0 ? <>Refreshing in {remainingSeconds} secs</> : null}
{activeRequests > 0 ? <>Fetching data... {countStr}</> : null}
</span>
</div>
</TooltipProvider>
);
};

Expand Down
3 changes: 1 addition & 2 deletions frontend/src/components/misc/page-content.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { Stack } from '@redpanda-data/ui';
import { motion } from 'motion/react';
import type { ReactNode } from 'react';

Expand All @@ -12,7 +11,7 @@ export type PageContentProps = {
function PageContent(props: PageContentProps) {
return (
<motion.div {...animProps} className={props.className}>
<Stack gap={3}>{props.children}</Stack>
<div className="flex flex-col gap-3">{props.children}</div>
</motion.div>
);
}
Expand Down
Loading