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
2 changes: 1 addition & 1 deletion ui/consumer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
},
"dependencies": {
"@datum-cloud/activity-ui": "0.7.1",
"@datum-cloud/datum-ui": "3.8.0",
"@datum-cloud/datum-ui": "3.11.0",
"@tanstack/react-query": "5.102.8",
"@tanstack/react-table": "^9.3.1",
"@tanstack/react-virtual": "^3.14.26",
Expand Down
16 changes: 8 additions & 8 deletions ui/consumer/pnpm-lock.yaml

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

89 changes: 46 additions & 43 deletions ui/consumer/src/components/instance-logs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* workload has a published HTTPProxy.
*/
import { ApiError } from '../lib/api';
import { useComputeLogsUrlState } from '../lib/logs-url-state';
import {
ALB_LOGS_PREVIEW_LIMIT,
combinedLogFacets,
Expand All @@ -17,6 +18,8 @@ import {
recentLogsRange,
useInstanceLogs,
useWorkloadLogs,
instanceUpstreamIPs,
useWindowLogFacets,
} from '../lib/o11y-logs';
import { Badge } from '@datum-cloud/datum-ui/badge';
import { useCopyToClipboard } from '@datum-cloud/datum-ui/hooks';
Expand All @@ -32,11 +35,9 @@ import { Icon, SpinnerIcon } from '@datum-cloud/datum-ui/icons';
import {
facetsFromEntries,
httpStatusBadgeType,
lastThirtyMinutes,
logRequestHost,
Logs,
parseLogLine,
resolveLogTimeRange,
type LogColumn,
type LogColumnSpec,
type LogEntry,
Expand All @@ -46,7 +47,7 @@ import {
import { cn } from '@datum-cloud/datum-ui/utils';
import { formatDistanceToNowStrict } from 'date-fns';
import { CheckIcon, CopyIcon, LogsIcon, RadioIcon } from 'lucide-react';
import { useCallback, useMemo, useState } from 'react';
import { useMemo, useState } from 'react';
import { Link } from 'react-router';
import { OPEN_ROW_PADDING } from './detail-list';
import { QuietNotice, SectionFrame } from './surface';
Expand Down Expand Up @@ -394,16 +395,9 @@ export function InstanceLogsExplorer({
upstreamIPs?: readonly string[];
className?: string;
}) {
const [filters, setFilters] = useState<LogFilters>({});
const [search, setSearch] = useState('');
const [live, setLive] = useState(false);
const [timeRange, setTimeRange] = useState<LogTimeRange>(() => lastThirtyMinutes());

const handleRefresh = useCallback(() => {
setTimeRange((current) =>
current.preset ? resolveLogTimeRange(current) : lastThirtyMinutes()
);
}, []);
// Time range, filters, search, live and the open row live in the URL.
const logsState = useComputeLogsUrlState();
const { timeRange, filters, search, live, refreshWindow } = logsState;

const logsQuery = useInstanceLogs(projectId, proxyId, instanceName, {
timeRange,
Expand All @@ -418,7 +412,21 @@ export function InstanceLogsExplorer({
() => filterCombinedLogs(logsQuery.data ?? [], filters),
[logsQuery.data, filters]
);
const facets = useMemo(() => combinedLogFacets(logsQuery.data ?? []), [logsQuery.data]);
// Every option in the window, whatever is ticked. Until those load, fall
// back to facets built from the fetched rows.
const ips = useMemo(() => instanceUpstreamIPs({ internalIPs: upstreamIPs }), [upstreamIPs]);
const instanceNames = useMemo(() => (instanceName ? [instanceName] : []), [instanceName]);
const windowFacets = useWindowLogFacets(projectId, {
proxyId: ips.length > 0 ? proxyId : undefined,
upstreamIPs: ips,
instanceNames,
filters,
search,
timeRange,
live,
});
const rowFacets = useMemo(() => combinedLogFacets(logsQuery.data ?? []), [logsQuery.data]);
const facets = windowFacets ?? rowFacets;

const denied = isLogsDenied(logsQuery.error);
const errorMessage = logsQuery.error && !denied ? logsQuery.error.message : undefined;
Expand Down Expand Up @@ -447,25 +455,20 @@ export function InstanceLogsExplorer({
<Card
size="sm"
sectioned
className={cn('flex min-h-96 flex-col overflow-hidden', className)}
className={cn('flex min-h-96 flex-1 flex-col overflow-hidden', className)}
style={{ minHeight: '32rem' }}
data-testid="compute-plugin-instance-logs-explorer">
<CardContent padding="none" className="flex min-h-0 flex-1 flex-col">
<Logs.Root
entries={visibleEntries}
facets={facets}
{...logsState}
timeRange={timeRange}
filters={filters}
search={search}
live={live}
isLoading={logsQuery.isLoading}
isFetching={logsQuery.isFetching}
error={errorMessage}
columns={[...EXPLORER_COLUMNS]}
onTimeRangeChange={setTimeRange}
onFiltersChange={setFilters}
onSearchChange={setSearch}
onLiveChange={setLive}
onRefresh={handleRefresh}
onRefresh={refreshWindow}
className="bg-card flex min-h-0 flex-1 flex-col">
{/* Same class string as the portal's AlbLogsExplorer so the filters bar matches. */}
<Logs.Explorer className="bg-card **:data-[slot=logs-filters]:bg-card min-h-0 flex-1" />
Expand Down Expand Up @@ -523,16 +526,9 @@ export function WorkloadLogsExplorer({
instanceNames: readonly string[];
className?: string;
}) {
const [filters, setFilters] = useState<LogFilters>({});
const [search, setSearch] = useState('');
const [live, setLive] = useState(false);
const [timeRange, setTimeRange] = useState<LogTimeRange>(() => lastThirtyMinutes());

const handleRefresh = useCallback(() => {
setTimeRange((current) =>
current.preset ? resolveLogTimeRange(current) : lastThirtyMinutes()
);
}, []);
// Time range, filters, search, live and the open row live in the URL.
const logsState = useComputeLogsUrlState();
const { timeRange, filters, search, live, refreshWindow } = logsState;

const queryFilters = useMemo(() => {
if (!filters[COMPUTE_LOG_INSTANCE_LABEL]) return filters;
Expand All @@ -553,14 +549,26 @@ export function WorkloadLogsExplorer({
() => filterWorkloadExplorerLogs(logsQuery.data ?? [], filters),
[logsQuery.data, filters]
);
const facets = useMemo(() => {
// Every option in the window, whatever is ticked, with every instance
// listed. Until those load, fall back to facets built from the fetched rows.
const windowFacets = useWindowLogFacets(projectId, {
proxyId,
instanceNames,
instanceFacet: true,
filters,
search,
timeRange,
live,
});
const rowFacets = useMemo(() => {
const entries = logsQuery.data ?? [];
const instance = facetsFromEntries(entries, [COMPUTE_LOG_INSTANCE_LABEL]).map((facet) => ({
...facet,
label: 'Instance',
}));
return [...combinedLogFacets(entries), ...instance];
}, [logsQuery.data]);
const facets = windowFacets ?? rowFacets;

const denied = isLogsDenied(logsQuery.error);
const errorMessage = logsQuery.error && !denied ? logsQuery.error.message : undefined;
Expand Down Expand Up @@ -589,25 +597,20 @@ export function WorkloadLogsExplorer({
<Card
size="sm"
sectioned
className={cn('flex min-h-96 flex-col overflow-hidden', className)}
className={cn('flex min-h-96 flex-1 flex-col overflow-hidden', className)}
style={{ minHeight: '32rem' }}
data-testid="compute-plugin-workload-logs-explorer">
<CardContent padding="none" className="flex min-h-0 flex-1 flex-col">
<Logs.Root
entries={visibleEntries}
facets={facets}
{...logsState}
timeRange={timeRange}
filters={filters}
search={search}
live={live}
isLoading={logsQuery.isLoading}
isFetching={logsQuery.isFetching}
error={errorMessage}
columns={[...WORKLOAD_EXPLORER_COLUMNS]}
onTimeRangeChange={setTimeRange}
onFiltersChange={setFilters}
onSearchChange={setSearch}
onLiveChange={setLive}
onRefresh={handleRefresh}
onRefresh={refreshWindow}
className="bg-card flex min-h-0 flex-1 flex-col">
<Logs.Explorer className="bg-card **:data-[slot=logs-filters]:bg-card min-h-0 flex-1" />
</Logs.Root>
Expand Down
2 changes: 1 addition & 1 deletion ui/consumer/src/components/instance-page-chrome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export function InstancePageChrome({
children: React.ReactNode;
}) {
return (
<div data-testid="compute-plugin-instance-detail" className="flex min-w-0 flex-col gap-6">
<div data-testid="compute-plugin-instance-detail" className="flex min-w-0 flex-1 flex-col gap-6">
<Breadcrumb className="min-w-0 overflow-x-auto">
<BreadcrumbList className="flex-nowrap">
<BreadcrumbItem>
Expand Down
7 changes: 4 additions & 3 deletions ui/consumer/src/components/skeletons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,10 @@ import { ChevronDown, ChevronUp } from 'lucide-react';
import type { CSSProperties } from 'react';

const PANEL_STYLE = { height: '27rem' } as const;
const EXPLORER_STYLE = { height: '32rem' } as const;
const EXPLORER_STYLE = { flex: '1 1 auto', minHeight: '32rem' } as const;
const CHART_STYLE = { height: 224 } as const;
const TOPOLOGY_STYLE = { height: '28rem' } as const;
// Topology and the log explorers fill the page, so their skeletons do too.
const TOPOLOGY_STYLE = { flex: '1 0 auto', minHeight: 'calc(28rem + 3rem)' } as const;

/**
* The list table's columns before usage data arrives, each with a bone the
Expand Down Expand Up @@ -374,7 +375,7 @@ export function WorkloadOverviewSkeleton() {
/** Matches the Topology tab: one diagram panel. */
export function WorkloadTopologySkeleton() {
return (
<div aria-busy="true" data-testid="compute-plugin-loading-workload-topology">
<div aria-busy="true" data-testid="compute-plugin-loading-workload-topology" className="flex flex-1 flex-col">
<Panel style={TOPOLOGY_STYLE}>
<Bone className="min-h-0 w-full flex-1" />
</Panel>
Expand Down
34 changes: 28 additions & 6 deletions ui/consumer/src/components/topology-canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,10 @@ const COMPACT_LAYOUT = { columns: 1, limit: 3 } as const;
* the page scrolls as usual and the canvas never traps a touch.
*/
const COMPACT_BREAKPOINT = 560;
/** Fixed frame height outside compact mode. Inline: the host does not compile `h-[28rem]`. */
/**
* Smallest frame outside compact mode. The frame otherwise grows to fill the
* space its card gives it. Inline: the host does not compile `min-h-[28rem]`.
*/
const FRAME_HEIGHT = '28rem';
/** Compact top inset, clearing the corner chips. */
const COMPACT_TOP = 52;
Expand Down Expand Up @@ -555,6 +558,8 @@ export function TopologyCanvas({
const [expanded, setExpanded] = useState<ReadonlySet<string>>(() => new Set());
const [compact, setCompact] = useState(false);
const [compactHeight, setCompactHeight] = useState(0);
// Set once someone pans or zooms, so a resize doesn't throw their view away.
const touchedRef = useRef(false);
viewRef.current = view;

useLayoutEffect(() => {
Expand Down Expand Up @@ -659,10 +664,19 @@ export function TopologyCanvas({
const world = worldRef.current;
if (!viewport || !world) return;
if (!compact) {
const next = fitView(viewport, world);
viewRef.current = next;
setView(next);
return;
// The frame fills its card, so its size follows the window. Refit as it
// changes until someone pans or zooms; Reset view refits on demand.
touchedRef.current = false;
const fit = () => {
if (touchedRef.current) return;
const next = fitView(viewport, world);
viewRef.current = next;
setView(next);
};
fit();
const observer = new ResizeObserver(fit);
observer.observe(viewport);
return () => observer.disconnect();
}
// Compact: track the tree's size, since the frame grows with it.
const apply = () => {
Expand Down Expand Up @@ -695,6 +709,7 @@ export function TopologyCanvas({
event.clientY - rect.top,
current.scale * Math.exp(-event.deltaY * 0.01)
);
touchedRef.current = true;
viewRef.current = next;
setView(next);
};
Expand All @@ -721,6 +736,7 @@ export function TopologyCanvas({
x: origin.x + move.clientX - startX,
y: origin.y + move.clientY - startY,
};
touchedRef.current = true;
viewRef.current = next;
setView(next);
};
Expand Down Expand Up @@ -749,6 +765,7 @@ export function TopologyCanvas({
viewport.clientHeight / 2,
viewRef.current.scale * factor
);
touchedRef.current = true;
viewRef.current = next;
setView(next);
};
Expand All @@ -758,6 +775,7 @@ export function TopologyCanvas({
const world = worldRef.current;
if (!viewport || !world) return;
const next = fitView(viewport, world);
touchedRef.current = false;
viewRef.current = next;
setView(next);
};
Expand All @@ -767,7 +785,11 @@ export function TopologyCanvas({
ref={viewportRef}
data-testid="compute-plugin-topology-canvas"
className={`cpt-root${panning ? ' cpt-panning' : ''}${compact ? ' cpt-compact' : ''}`}
style={{ position: 'relative', height: compact ? compactHeight || FRAME_HEIGHT : FRAME_HEIGHT }}
style={
compact
? { position: 'relative', height: compactHeight || FRAME_HEIGHT }
: { position: 'relative', flex: '1 1 auto', minHeight: FRAME_HEIGHT }
}
aria-label={
compact
? 'Topology diagram.'
Expand Down
Loading
Loading