Skip to content

Commit 662ae6d

Browse files
devin-ai-integration[bot]Convex, Inc.
authored and
Convex, Inc.
committedApr 4, 2025·
dashboard: Add logs tab to FunctionsView (#36192)
Added a logs section to FunctionsView.tsx that shows logs filtered to the currently selected function. Requirements: - Added a new tab in FunctionsView.tsx to display function-specific logs - Reused the LogList component from Logs.tsx - Filtered logs to only show entries related to the current function Also adds new styling to the tabs on the schedules page GitOrigin-RevId: 11913e5d7c4951dcb336930a3f986434d6ab7c4f
1 parent 30e5673 commit 662ae6d

File tree

9 files changed

+258
-58
lines changed

9 files changed

+258
-58
lines changed
 

‎npm-packages/dashboard-common/src/elements/Tab.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { Tab as HeadlessTab } from "@headlessui/react";
2-
import classNames from "classnames";
32
import { Fragment, PropsWithChildren } from "react";
43
import { Button, ButtonProps } from "@common/elements/Button";
4+
import { cn } from "@common/lib/cn";
55

66
export function Tab({
77
disabled,
88
tip,
99
children,
1010
large = false,
11+
className,
1112
...props
1213
}: ButtonProps &
1314
PropsWithChildren<{ disabled?: boolean; tip?: string; large?: boolean }>) {
@@ -18,7 +19,7 @@ export function Tab({
1819
disabled={disabled}
1920
tip={tip}
2021
variant="unstyled"
21-
className={classNames(
22+
className={cn(
2223
"p-2 text-sm rounded whitespace-nowrap cursor-pointer",
2324
!disabled && selected
2425
? "text-content-primary"
@@ -31,6 +32,7 @@ export function Tab({
3132
// It's OK for tabs.
3233
// eslint-disable-next-line no-restricted-syntax
3334
large && "text-lg",
35+
className,
3436
)}
3537
{...props}
3638
>

‎npm-packages/dashboard-common/src/features/functions/components/DirectorySidebar.tsx

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function DirectorySidebar({
2727
)}
2828
ref={ref}
2929
>
30-
<div className="flex flex-col px-3">
30+
<div className="mb-2 flex flex-col px-3">
3131
<NentSwitcher />
3232
<h5>Function Explorer</h5>
3333
</div>

‎npm-packages/dashboard-common/src/features/functions/components/FileEditor.tsx

+6-2
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,11 @@ export function FileEditor({
4141
}, [router.events]);
4242

4343
return (
44-
<Sheet className="h-full overflow-hidden py-2" padding={false} ref={ref}>
44+
<Sheet
45+
className="max-h-full w-full overflow-y-auto py-2"
46+
padding={false}
47+
ref={ref}
48+
>
4549
<div className="grow">
4650
{sourceCode === undefined ? (
4751
<div className="my-20">
@@ -64,7 +68,7 @@ export function FileEditor({
6468
}
6569
: undefined
6670
}
67-
height={{ type: "content", maxHeightRem: 30 }}
71+
height={{ type: "parent" }}
6872
/>
6973
)}
7074
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { useContext, useState } from "react";
2+
import { useDebounce, useLocalStorage } from "react-use";
3+
import { TextInput } from "@common/elements/TextInput";
4+
import { LogList } from "@common/features/logs/components/LogList";
5+
import { LogToolbar } from "@common/features/logs/components/LogToolbar";
6+
import { filterLogs } from "@common/features/logs/lib/filterLogs";
7+
import { displayNameToIdentifier } from "@common/lib/functions/FunctionsProvider";
8+
import { functionIdentifierValue } from "@common/lib/functions/generateFileTree";
9+
import { UdfLog, useLogs } from "@common/lib/useLogs";
10+
import { ModuleFunction } from "@common/lib/functions/types";
11+
import { Nent } from "@common/lib/useNents";
12+
import { Button } from "@common/elements/Button";
13+
import { ExternalLinkIcon } from "@radix-ui/react-icons";
14+
import { useRouter } from "next/router";
15+
import { DeploymentInfoContext } from "@common/lib/deploymentContext";
16+
17+
type LogLevel = "success" | "failure" | "DEBUG" | "INFO" | "WARN" | "ERROR";
18+
19+
const DEFAULT_LOG_LEVELS: LogLevel[] = [
20+
"success",
21+
"failure",
22+
"DEBUG",
23+
"INFO",
24+
"WARN",
25+
"ERROR",
26+
];
27+
28+
interface FunctionLogsProps {
29+
currentOpenFunction: ModuleFunction;
30+
selectedNent?: Nent;
31+
}
32+
33+
export function FunctionLogs({
34+
currentOpenFunction,
35+
selectedNent,
36+
}: FunctionLogsProps) {
37+
const functionId = functionIdentifierValue(
38+
displayNameToIdentifier(currentOpenFunction.displayName),
39+
selectedNent?.path,
40+
);
41+
42+
const [logs, setLogs] = useState<UdfLog[]>([]);
43+
const [paused, setPaused] = useState<number>(0);
44+
const [manuallyPaused, setManuallyPaused] = useState(false);
45+
46+
// Store filter and selected levels in local storage, scoped to the function
47+
const [filter, setFilter] = useLocalStorage<string>(
48+
`function-logs/${functionId}/filter`,
49+
"",
50+
);
51+
const [innerFilter, setInnerFilter] = useState(filter ?? "");
52+
const [selectedLevels, setSelectedLevels] = useLocalStorage<LogLevel[]>(
53+
`function-logs/${functionId}/selected-levels`,
54+
DEFAULT_LOG_LEVELS,
55+
);
56+
57+
useDebounce(
58+
() => {
59+
setFilter(innerFilter);
60+
},
61+
200,
62+
[innerFilter],
63+
);
64+
65+
const onPause = (p: boolean) => {
66+
const now = new Date().getTime();
67+
setPaused(p ? now : 0);
68+
};
69+
70+
const logsConnectivityCallbacks = {
71+
onReconnected: () => {},
72+
onDisconnected: () => {},
73+
};
74+
75+
const receiveLogs = (entries: UdfLog[]) => {
76+
setLogs((prev) => {
77+
const newLogs = filterLogs(
78+
{
79+
logTypes: DEFAULT_LOG_LEVELS,
80+
functions: [functionId],
81+
selectedFunctions: [functionId],
82+
filter: "",
83+
},
84+
entries,
85+
);
86+
if (!newLogs) {
87+
return prev;
88+
}
89+
return [...prev, ...newLogs].slice(
90+
Math.max(prev.length + entries.length - 10000, 0),
91+
prev.length + entries.length,
92+
);
93+
});
94+
};
95+
96+
useLogs(logsConnectivityCallbacks, receiveLogs, paused > 0 || manuallyPaused);
97+
98+
const router = useRouter();
99+
const { deploymentsURI } = useContext(DeploymentInfoContext);
100+
101+
return (
102+
<div className="flex h-full w-full min-w-[48rem] grow flex-col gap-2">
103+
<LogToolbar
104+
functions={[functionId]}
105+
selectedFunctions={[functionId]}
106+
setSelectedFunctions={(_functions) => {}}
107+
selectedLevels={selectedLevels ?? DEFAULT_LOG_LEVELS}
108+
setSelectedLevels={(levels) => setSelectedLevels(levels as LogLevel[])}
109+
selectedNents={selectedNent ? [selectedNent.path] : []}
110+
setSelectedNents={() => {}}
111+
hideFunctionFilter
112+
firstItem={
113+
<div className="flex grow gap-2">
114+
<Button
115+
variant="neutral"
116+
size="sm"
117+
icon={<ExternalLinkIcon />}
118+
href={`${deploymentsURI}/logs${router.query.component ? `?component=${router.query.component}` : ""}`}
119+
>
120+
View all Logs
121+
</Button>
122+
<TextInput
123+
id="Search logs"
124+
placeholder="Filter logs..."
125+
value={innerFilter}
126+
onChange={(e) => setInnerFilter(e.target.value)}
127+
type="search"
128+
/>
129+
</div>
130+
}
131+
/>
132+
<LogList
133+
nents={selectedNent ? [selectedNent] : []}
134+
logs={logs}
135+
filteredLogs={filterLogs(
136+
{
137+
logTypes: selectedLevels ?? DEFAULT_LOG_LEVELS,
138+
functions: [functionId],
139+
selectedFunctions: [functionId],
140+
filter: filter ?? "",
141+
},
142+
logs,
143+
)}
144+
deploymentAuditLogs={[]}
145+
filter={filter ?? ""}
146+
clearedLogs={[]}
147+
setClearedLogs={() => {}}
148+
paused={paused > 0 || manuallyPaused}
149+
setPaused={onPause}
150+
setManuallyPaused={(p) => {
151+
onPause(p);
152+
setManuallyPaused(p);
153+
}}
154+
/>
155+
</div>
156+
);
157+
}

‎npm-packages/dashboard-common/src/features/functions/components/FunctionSummary.tsx

+14-15
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { PlayIcon } from "@radix-ui/react-icons";
22
import { useQuery } from "convex/react";
3-
import classNames from "classnames";
43
import { useContext, useState } from "react";
54
import { useSessionStorage } from "react-use";
65
import { lt } from "semver";
@@ -54,7 +53,7 @@ export function FunctionSummary({
5453
return <Loading />;
5554
}
5655
return (
57-
<div className={classNames("flex h-full flex-col overflow-hidden")}>
56+
<div className="flex h-full flex-col overflow-hidden">
5857
<div className="flex items-end justify-between gap-2 pb-2">
5958
{showEnableProdEditsModal && (
6059
<ProductionEditsConfirmationDialog
@@ -69,20 +68,20 @@ export function FunctionSummary({
6968
/>
7069
)}
7170
<div className="flex items-center gap-2">
72-
<div className="flex flex-col items-start gap-1">
73-
<div className="flex items-center gap-3">
74-
<h3 className="font-mono">{currentOpenFunction.name}</h3>
75-
<div className="rounded border px-1 py-0.5 text-xs font-semibold text-content-primary">
76-
{functionTypeLabel(currentOpenFunction.udfType)}
77-
</div>
71+
<div className="flex items-center gap-3">
72+
<h3 className="font-mono">{currentOpenFunction.name}</h3>
73+
<div className="rounded border p-1 text-xs font-semibold text-content-primary">
74+
{currentOpenFunction.visibility.kind === "internal" &&
75+
"Internal "}
76+
{functionTypeLabel(currentOpenFunction.udfType)}
7877
</div>
79-
{currentOpenFunction.displayName !== currentOpenFunction.name && (
80-
<CopyTextButton
81-
className="font-mono"
82-
text={currentOpenFunction.displayName}
83-
/>
84-
)}
8578
</div>
79+
{currentOpenFunction.displayName !== currentOpenFunction.name && (
80+
<CopyTextButton
81+
className="font-mono"
82+
text={currentOpenFunction.displayName}
83+
/>
84+
)}
8685
</div>
8786
{
8887
// Supported UDF types for in-dashboard testing
@@ -113,7 +112,7 @@ export function FunctionSummary({
113112
: setShowEnableProdEditsModal(true)
114113
}
115114
icon={<PlayIcon />}
116-
size="sm"
115+
size="xs"
117116
variant="primary"
118117
>
119118
Run Function

‎npm-packages/dashboard-common/src/features/functions/components/FunctionsView.tsx

+40-9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useContext } from "react";
1+
import { useContext, useState } from "react";
22
import { CodeIcon } from "@radix-ui/react-icons";
33
import {
44
useCurrentOpenFunction,
@@ -12,6 +12,10 @@ import { DeploymentInfoContext } from "@common/lib/deploymentContext";
1212
import { SidebarDetailLayout } from "@common/layouts/SidebarDetailLayout";
1313
import { EmptySection } from "@common/elements/EmptySection";
1414
import { DeploymentPageTitle } from "@common/elements/DeploymentPageTitle";
15+
import { Tab } from "@common/elements/Tab";
16+
import { Tab as HeadlessTab } from "@headlessui/react";
17+
import { useNents } from "@common/lib/useNents";
18+
import { FunctionLogs } from "./FunctionLogs";
1519

1620
export function FunctionsView() {
1721
return (
@@ -26,6 +30,8 @@ function Functions() {
2630
const deploymentId = useCurrentDeployment()?.id;
2731
const currentOpenFunction = useCurrentOpenFunction();
2832
const modules = useModuleFunctions();
33+
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
34+
const { selectedNent } = useNents();
2935

3036
if (modules.length === 0) {
3137
return <EmptyFunctions />;
@@ -40,16 +46,41 @@ function Functions() {
4046
);
4147
} else {
4248
content = (
43-
<div className="flex h-fit max-w-[110rem] flex-col gap-3 p-6 py-4">
44-
<div className="flex-none">
49+
<div className="flex h-full max-w-[110rem] grow flex-col">
50+
<div className="flex-none bg-background-secondary px-6 pt-4">
4551
<FunctionSummary currentOpenFunction={currentOpenFunction} />
4652
</div>
47-
<div className="flex-none">
48-
<PerformanceGraphs />
49-
</div>
50-
<div>
51-
<FileEditor moduleFunction={currentOpenFunction} />
52-
</div>
53+
54+
<HeadlessTab.Group
55+
selectedIndex={selectedTabIndex}
56+
onChange={setSelectedTabIndex}
57+
className="flex grow flex-col"
58+
as="div"
59+
>
60+
<div className="-ml-2 mb-6 flex gap-2 border-b bg-background-secondary px-6">
61+
<Tab>Statistics</Tab>
62+
<Tab>Code</Tab>
63+
<Tab>Logs</Tab>
64+
</div>
65+
66+
<HeadlessTab.Panels className="flex w-full grow px-6 pb-4">
67+
<HeadlessTab.Panel className="grow">
68+
<PerformanceGraphs />
69+
</HeadlessTab.Panel>
70+
71+
<HeadlessTab.Panel className="grow">
72+
<FileEditor moduleFunction={currentOpenFunction} />
73+
</HeadlessTab.Panel>
74+
75+
<HeadlessTab.Panel className="grow">
76+
<FunctionLogs
77+
key={currentOpenFunction.displayName}
78+
currentOpenFunction={currentOpenFunction}
79+
selectedNent={selectedNent || undefined}
80+
/>
81+
</HeadlessTab.Panel>
82+
</HeadlessTab.Panels>
83+
</HeadlessTab.Group>
5384
</div>
5485
);
5586
}

‎npm-packages/dashboard-common/src/features/logs/components/LogList.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ export function LogList({
9090
},
9191
[paused, setPaused],
9292
);
93+
9394
return (
9495
<div className="flex h-full w-full flex-auto flex-col gap-2 overflow-hidden">
9596
{shownLog && logs && (
@@ -101,7 +102,7 @@ export function LogList({
101102
/>
102103
)}
103104
{interleavedLogs !== undefined && (
104-
<Sheet className="h-full w-full" padding={false} ref={sheetRef}>
105+
<Sheet className="min-h-full w-full" padding={false} ref={sheetRef}>
105106
{heightOfListContainer !== 0 && (
106107
<WindowedLogList
107108
{...{

‎npm-packages/dashboard-common/src/features/logs/components/LogToolbar.tsx

+25-21
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export function LogToolbar({
1515
selectedNents,
1616
setSelectedNents,
1717
firstItem,
18+
hideFunctionFilter = false,
1819
}: {
1920
functions: string[];
2021
selectedFunctions: string[];
@@ -25,6 +26,7 @@ export function LogToolbar({
2526
selectedNents: string[];
2627
setSelectedNents(newValue: string[]): void;
2728
firstItem?: React.ReactNode;
29+
hideFunctionFilter?: boolean;
2830
}) {
2931
return (
3032
<div className="flex w-full flex-wrap items-center justify-end gap-2">
@@ -49,27 +51,29 @@ export function LogToolbar({
4951
/>
5052
</div>
5153
)}
52-
<div className="min-w-[9.5rem]">
53-
<MultiSelectCombobox
54-
options={functionsForSelectedNents(selectedNents, functions)}
55-
selectedOptions={functionsForSelectedNents(
56-
selectedNents,
57-
selectedFunctions,
58-
)}
59-
processFilterOption={(option) => {
60-
const id = functionIdentifierFromValue(option);
61-
return id.componentPath
62-
? `${id.componentPath}/${id.identifier}`
63-
: id.identifier;
64-
}}
65-
setSelectedOptions={setSelectedFunctions}
66-
unit="function"
67-
unitPlural="functions"
68-
label="Functions"
69-
labelHidden
70-
Option={FunctionNameOption}
71-
/>
72-
</div>
54+
{!hideFunctionFilter && (
55+
<div className="min-w-[9.5rem]">
56+
<MultiSelectCombobox
57+
options={functionsForSelectedNents(selectedNents, functions)}
58+
selectedOptions={functionsForSelectedNents(
59+
selectedNents,
60+
selectedFunctions,
61+
)}
62+
processFilterOption={(option) => {
63+
const id = functionIdentifierFromValue(option);
64+
return id.componentPath
65+
? `${id.componentPath}/${id.identifier}`
66+
: id.identifier;
67+
}}
68+
setSelectedOptions={setSelectedFunctions}
69+
unit="function"
70+
unitPlural="functions"
71+
label="Functions"
72+
labelHidden
73+
Option={FunctionNameOption}
74+
/>
75+
</div>
76+
)}
7377
<div className="min-w-[9.5rem]">
7478
<MultiSelectCombobox
7579
options={["success", "failure", "DEBUG", "INFO", "WARN", "ERROR"]}

‎npm-packages/dashboard-common/src/layouts/SchedulingLayout.tsx

+9-7
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,19 @@ export function SchedulingLayout({ children }: { children: React.ReactNode }) {
1111
const currentPage = pathParts[pathParts.length - 1];
1212

1313
return (
14-
<div className="flex h-full flex-col p-6 py-4">
15-
<div className="w-fit min-w-60">
16-
<NentSwitcher />
14+
<div className="flex h-full flex-col">
15+
<div className="flex w-full items-center gap-4 bg-background-secondary px-6 pt-4">
16+
<h3>Schedules</h3>
17+
{/* Negative margin accounting for the margin on NentSwitcher */}
18+
<div className="-mb-4 w-fit min-w-60">
19+
<NentSwitcher />
20+
</div>
1721
</div>
18-
<div className="-ml-2 mb-4 flex gap-4">
22+
<div className="mb-4 flex gap-2 border-b bg-background-secondary px-4 pt-2">
1923
<HeadlessTab.Group
2024
selectedIndex={currentPage.startsWith("functions") ? 0 : 1}
2125
>
2226
<Tab
23-
large
2427
href={{
2528
pathname: `${basePath}/functions`,
2629
query,
@@ -29,7 +32,6 @@ export function SchedulingLayout({ children }: { children: React.ReactNode }) {
2932
Scheduled Functions
3033
</Tab>
3134
<Tab
32-
large
3335
href={{
3436
pathname: `${basePath}/crons`,
3537
query,
@@ -39,7 +41,7 @@ export function SchedulingLayout({ children }: { children: React.ReactNode }) {
3941
</Tab>
4042
</HeadlessTab.Group>
4143
</div>
42-
<div className="grow">{children}</div>
44+
<div className="mx-6 mb-4 grow">{children}</div>
4345
</div>
4446
);
4547
}

0 commit comments

Comments
 (0)
Please sign in to comment.