@sentry/junior-dashboard 0.264.0 → 0.265.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +2 -5
- package/dist/app.js +119 -35
- package/dist/client.js +145 -136
- package/dist/index.d.ts +0 -1
- package/dist/index.js +119 -35
- package/dist/tailwind.css +1 -1
- package/package.json +5 -4
- package/src/api/schema.ts +0 -1
- package/src/app.ts +5 -20
- package/src/client/App.tsx +29 -31
- package/src/client/api.ts +7 -0
- package/src/client/components/BottomSheet.tsx +12 -2
- package/src/client/components/CopyButton.tsx +83 -0
- package/src/client/components/DetailsPanel.tsx +133 -0
- package/src/client/components/Drawer.tsx +15 -24
- package/src/client/components/FilterBar.tsx +16 -31
- package/src/client/components/Metric.tsx +33 -23
- package/src/client/components/PageContentSkeleton.tsx +1 -1
- package/src/client/components/Pagination.tsx +1 -1
- package/src/client/components/ParticipantAvatarStack.tsx +40 -36
- package/src/client/components/ProfileMenu.tsx +27 -93
- package/src/client/components/README.md +86 -77
- package/src/client/components/SegmentedNav.tsx +1 -1
- package/src/client/components/ShimmerText.tsx +2 -6
- package/src/client/components/ThemeSwitcher.tsx +19 -15
- package/src/client/components/charts/ActivityChart.tsx +173 -288
- package/src/client/components/charts/CacheWrites.tsx +63 -50
- package/src/client/components/charts/InputCacheChart.tsx +137 -131
- package/src/client/components/charts/README.md +4 -4
- package/src/client/components/charts/SystemMetricCharts.tsx +68 -95
- package/src/client/components/charts/WorkspaceUsageChart.tsx +42 -62
- package/src/client/components/controls/DirectorySortSelect.tsx +3 -3
- package/src/client/components/controls/TimeRangeSelector.tsx +10 -15
- package/src/client/components/layout/DashboardHeader.tsx +44 -90
- package/src/client/components/layout/SecondaryNavigation.tsx +2 -1
- package/src/client/components/layout/VisualViewportShell.tsx +2 -2
- package/src/client/components/ui/accordion.tsx +77 -0
- package/src/client/components/ui/alert-dialog.tsx +89 -0
- package/src/client/components/ui/button.tsx +85 -0
- package/src/client/components/ui/chart.tsx +74 -0
- package/src/client/components/ui/checkbox.tsx +26 -0
- package/src/client/components/ui/collapsible.tsx +22 -0
- package/src/client/components/ui/field.tsx +49 -0
- package/src/client/components/ui/input.tsx +35 -0
- package/src/client/components/ui/native-select.tsx +57 -0
- package/src/client/components/ui/popover.tsx +67 -0
- package/src/client/components/ui/radio-group.tsx +32 -0
- package/src/client/components/ui/sheet.tsx +13 -3
- package/src/client/components/ui/skeleton.tsx +19 -0
- package/src/client/components/ui/table.tsx +80 -0
- package/src/client/components/ui/tabs.tsx +75 -0
- package/src/client/components/ui/textarea.tsx +17 -0
- package/src/client/components/ui/toast.tsx +215 -0
- package/src/client/components/ui/toggle-group.tsx +36 -0
- package/src/client/components/ui/toggle.tsx +37 -0
- package/src/client/components/ui/tooltip.tsx +168 -0
- package/src/client/components/ui/trace.tsx +96 -0
- package/src/client/conversations/ChatLayout.tsx +7 -3
- package/src/client/conversations/ConversationBrief.tsx +17 -6
- package/src/client/conversations/ConversationComposer.tsx +26 -12
- package/src/client/conversations/ConversationDetails.tsx +147 -62
- package/src/client/conversations/ConversationEventDetails.tsx +141 -0
- package/src/client/conversations/ConversationEventLog.tsx +150 -129
- package/src/client/conversations/ConversationHeader.tsx +8 -3
- package/src/client/conversations/ConversationHeaderActions.tsx +90 -82
- package/src/client/conversations/ConversationHomeList.tsx +1 -1
- package/src/client/conversations/ConversationLinkedWork.tsx +118 -0
- package/src/client/conversations/ConversationMeta.tsx +55 -88
- package/src/client/conversations/ConversationPage.tsx +220 -101
- package/src/client/conversations/ConversationSidebar.tsx +48 -29
- package/src/client/conversations/ConversationWatches.tsx +165 -0
- package/src/client/conversations/ConversationWorkspace.tsx +12 -22
- package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -1
- package/src/client/conversations/DetailsList.tsx +124 -0
- package/src/client/conversations/EventDetails.tsx +42 -8
- package/src/client/conversations/ForkConversationButton.tsx +27 -16
- package/src/client/conversations/ObjectCard.tsx +20 -8
- package/src/client/conversations/PendingMailboxStack.tsx +52 -33
- package/src/client/conversations/README.md +86 -13
- package/src/client/conversations/ShareConversationButton.tsx +25 -13
- package/src/client/conversations/SubagentTranscriptDrawer.tsx +21 -15
- package/src/client/conversations/ToolFrame.tsx +36 -52
- package/src/client/conversations/TranscriptActivityGroup.tsx +13 -4
- package/src/client/conversations/TranscriptAutomationTriggerView.tsx +5 -4
- package/src/client/conversations/TranscriptContextEventView.tsx +25 -11
- package/src/client/conversations/TranscriptHeadingRow.tsx +7 -7
- package/src/client/conversations/TranscriptLoading.tsx +1 -1
- package/src/client/conversations/TranscriptMarkdown.tsx +25 -20
- package/src/client/conversations/TranscriptMessageContextView.tsx +8 -2
- package/src/client/conversations/TranscriptMessageView.tsx +6 -1
- package/src/client/conversations/TranscriptReasoningView.tsx +54 -68
- package/src/client/conversations/TranscriptTimestamp.tsx +9 -7
- package/src/client/conversations/TranscriptToolView.tsx +1 -1
- package/src/client/conversations/TranscriptTurnContextView.tsx +99 -27
- package/src/client/conversations/TranscriptView.tsx +63 -27
- package/src/client/conversations/eventLog.ts +57 -0
- package/src/client/conversations/eventTime.ts +60 -0
- package/src/client/conversations/queries.ts +29 -0
- package/src/client/conversations/transcriptBottomPinning.ts +34 -0
- package/src/client/format.ts +5 -0
- package/src/client/pages/ConnectionsPage.tsx +2 -2
- package/src/client/pages/PersonalTokensPage.tsx +4 -4
- package/src/client/pages/SettingsPage.tsx +9 -10
- package/src/client/pages/admin/ServiceAccountsSection.tsx +2 -2
- package/src/client/pages/automations/AutomationCostChart.tsx +50 -71
- package/src/client/pages/automations/AutomationDeleteDialog.tsx +87 -0
- package/src/client/pages/automations/AutomationDetails.tsx +193 -0
- package/src/client/pages/automations/AutomationEditPage.tsx +7 -8
- package/src/client/pages/automations/AutomationEditor.tsx +41 -29
- package/src/client/pages/automations/AutomationEditorLoading.tsx +1 -1
- package/src/client/pages/automations/AutomationEventFields.tsx +202 -119
- package/src/client/pages/automations/AutomationExecutionChart.tsx +51 -64
- package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +47 -70
- package/src/client/pages/automations/AutomationExecutionsPage.tsx +150 -126
- package/src/client/pages/automations/AutomationFilters.tsx +189 -74
- package/src/client/pages/automations/AutomationLifecycle.tsx +109 -78
- package/src/client/pages/automations/AutomationOutcomeFields.tsx +26 -24
- package/src/client/pages/automations/AutomationRecentRuns.tsx +143 -0
- package/src/client/pages/automations/AutomationRow.tsx +12 -138
- package/src/client/pages/automations/AutomationRunDetails.tsx +307 -0
- package/src/client/pages/automations/AutomationRunsPage.tsx +119 -94
- package/src/client/pages/automations/AutomationScheduleFields.tsx +51 -51
- package/src/client/pages/automations/AutomationStatus.tsx +201 -0
- package/src/client/pages/automations/AutomationVersionsPage.tsx +65 -28
- package/src/client/pages/automations/AutomationsPage.tsx +80 -68
- package/src/client/pages/automations/AutomationsPageLayout.tsx +46 -14
- package/src/client/pages/automations/README.md +57 -0
- package/src/client/pages/automations/TriggerFilters.tsx +22 -0
- package/src/client/pages/automations/automationFormat.ts +37 -23
- package/src/client/pages/code/CodeActivityChart.tsx +59 -101
- package/src/client/pages/code/CodePage.tsx +52 -55
- package/src/client/pages/code/RepositoryPage.tsx +6 -8
- package/src/client/pages/locations/LocationActivityChart.tsx +53 -70
- package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +60 -80
- package/src/client/pages/memory/MemoryCostChart.tsx +79 -103
- package/src/client/pages/memory/MemoryDetails.tsx +218 -0
- package/src/client/pages/memory/MemoryPage.tsx +168 -112
- package/src/client/pages/memory/MemoryPageLayout.tsx +33 -9
- package/src/client/pages/memory/MemoryTimeline.tsx +46 -69
- package/src/client/pages/memory/memoryRecord.ts +1 -1
- package/src/client/pages/people/ContributionGrid.tsx +23 -16
- package/src/client/pages/people/PeopleActivityChart.tsx +68 -107
- package/src/client/pages/people/ProfileCodeChanges.tsx +5 -5
- package/src/client/pages/system/BaseSandboxCard.tsx +1 -1
- package/src/client/pages/system/ConversationActivityChart.tsx +49 -67
- package/src/client/pages/system/GuardianActivity.tsx +1 -2
- package/src/client/pages/system/PluginBarChart.tsx +106 -87
- package/src/client/pages/system/PluginReportRecordSet.tsx +25 -24
- package/src/client/pages/system/README.md +23 -0
- package/src/client/pages/system/SystemNavigation.tsx +5 -1
- package/src/client/pages/system/WorkspaceDeleteDialog.tsx +111 -0
- package/src/client/pages/system/WorkspaceDetails.tsx +200 -43
- package/src/client/pages/system/WorkspaceEditor.tsx +59 -61
- package/src/client/pages/system/WorkspaceFormPage.tsx +23 -47
- package/src/client/pages/system/WorkspaceList.tsx +20 -30
- package/src/client/pages/system/WorkspacesPage.tsx +117 -119
- package/src/client/pages/system/sandboxSize.ts +8 -0
- package/src/client/pages/system/workspaceQueries.ts +53 -0
- package/src/client/pages/user/PluginUserPage.tsx +51 -2
- package/src/client/pages/user/pluginUserPageData.ts +1 -6
- package/src/client/styles.ts +4 -0
- package/src/client.tsx +7 -1
- package/src/mock-reporting/automation-collection.ts +4 -2
- package/src/mock-reporting/fixtures.ts +23 -7
- package/src/mock-reporting/routes.ts +12 -0
- package/src/mock-reporting/watches.ts +94 -0
- package/src/shell.ts +4 -9
- package/src/tailwind.css +0 -10
- package/src/client/components/Button.tsx +0 -118
- package/src/client/components/Field.tsx +0 -53
- package/src/client/components/SegmentedTabs.tsx +0 -119
- package/src/client/components/Select.tsx +0 -52
- package/src/client/components/Skeleton.tsx +0 -17
- package/src/client/components/TextInput.tsx +0 -58
- package/src/client/components/Tooltip.tsx +0 -216
- package/src/client/components/charts/useChartLayout.ts +0 -27
- package/src/client/components/ui/sheet.LICENSE +0 -21
- package/src/client/conversations/ConversationDetailsFrame.tsx +0 -119
- package/src/client/conversations/CopyMarkdownButton.tsx +0 -65
- package/src/client/pages/automations/AutomationDetailsDrawer.tsx +0 -219
- package/src/client/pages/dev/ChartFixtures.tsx +0 -325
- package/src/client/pages/dev/ComponentsPage.tsx +0 -899
- package/src/client/pages/dev/ConversationFixture.tsx +0 -231
- package/src/client/pages/dev/ObjectFixtures.tsx +0 -138
- package/src/client/pages/dev/SegmentedFixtures.tsx +0 -59
- package/src/client/pages/dev/SelectFixtures.tsx +0 -62
- package/src/client/pages/dev/TranscriptActivityFixtures.tsx +0 -206
- package/src/client/pages/memory/MemoryDetailsDrawer.tsx +0 -167
- package/src/client/pages/system/SandboxSizeSummary.tsx +0 -47
- package/src/client/pages/system/SnapshotSummary.tsx +0 -78
|
@@ -3,7 +3,8 @@ import { Monitor, Moon, Sun, type LucideIcon } from "lucide-react";
|
|
|
3
3
|
import type { DashboardThemeChoice } from "../../theme";
|
|
4
4
|
import { cn } from "../styles";
|
|
5
5
|
import { useDashboardThemeChoice } from "../theme";
|
|
6
|
-
import {
|
|
6
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
|
|
7
|
+
import { ToggleGroup, ToggleGroupItem } from "./ui/toggle-group";
|
|
7
8
|
|
|
8
9
|
const themeChoices = [
|
|
9
10
|
{ Icon: Monitor, label: "System", value: "system" },
|
|
@@ -25,24 +26,27 @@ export function ThemeSwitcher(props: { className?: string }) {
|
|
|
25
26
|
<span className="font-sans text-sm font-semibold text-dashboard-text">
|
|
26
27
|
Theme
|
|
27
28
|
</span>
|
|
28
|
-
<
|
|
29
|
+
<ToggleGroup
|
|
29
30
|
aria-label="Theme"
|
|
30
|
-
className="
|
|
31
|
-
|
|
31
|
+
className="gap-0.5 rounded-lg border border-dashboard-border p-0.5"
|
|
32
|
+
onValueChange={([next]) => {
|
|
33
|
+
if (next) setChoice(next);
|
|
34
|
+
}}
|
|
35
|
+
value={[choice]}
|
|
32
36
|
>
|
|
33
37
|
{themeChoices.map(({ Icon, label, value }) => (
|
|
34
|
-
<
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
<
|
|
43
|
-
</
|
|
38
|
+
<Tooltip key={value}>
|
|
39
|
+
<TooltipTrigger
|
|
40
|
+
render={
|
|
41
|
+
<ToggleGroupItem aria-label={`${label} theme`} value={value}>
|
|
42
|
+
<Icon aria-hidden="true" size={14} strokeWidth={2} />
|
|
43
|
+
</ToggleGroupItem>
|
|
44
|
+
}
|
|
45
|
+
/>
|
|
46
|
+
<TooltipContent size="compact">{label}</TooltipContent>
|
|
47
|
+
</Tooltip>
|
|
44
48
|
))}
|
|
45
|
-
</
|
|
49
|
+
</ToggleGroup>
|
|
46
50
|
</div>
|
|
47
51
|
);
|
|
48
52
|
}
|
|
@@ -1,62 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
2
|
cloneElement,
|
|
3
|
-
createContext,
|
|
4
3
|
type CSSProperties,
|
|
5
4
|
type ReactElement,
|
|
6
5
|
type ReactNode,
|
|
7
|
-
type SVGProps,
|
|
8
|
-
useContext,
|
|
9
|
-
useLayoutEffect,
|
|
10
|
-
useRef,
|
|
11
|
-
useState,
|
|
12
6
|
} from "react";
|
|
7
|
+
import { ReferenceLine, usePlotArea, XAxis, YAxis } from "recharts";
|
|
13
8
|
|
|
14
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
Tooltip,
|
|
11
|
+
TooltipContent,
|
|
12
|
+
TooltipTitle,
|
|
13
|
+
TooltipTrigger,
|
|
14
|
+
tooltipContentVariants,
|
|
15
|
+
} from "../ui/tooltip";
|
|
15
16
|
import { getDashboardTimeZone } from "../../format";
|
|
16
17
|
import { cn } from "../../styles";
|
|
17
18
|
|
|
18
|
-
/** Shared SVG dimensions and derived plot bounds for activity charts. */
|
|
19
|
-
export type ActivityChartLayout = {
|
|
20
|
-
bottom: number;
|
|
21
|
-
height: number;
|
|
22
|
-
left: number;
|
|
23
|
-
plotHeight: number;
|
|
24
|
-
plotWidth: number;
|
|
25
|
-
right: number;
|
|
26
|
-
top: number;
|
|
27
|
-
width: number;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
type LayoutOptions = {
|
|
31
|
-
bottom?: number;
|
|
32
|
-
left?: number;
|
|
33
|
-
right?: number;
|
|
34
|
-
top?: number;
|
|
35
|
-
width?: number;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
/** Create the shared plot dimensions used by dashboard activity charts. */
|
|
39
|
-
export function createActivityChartLayout(
|
|
40
|
-
height: number,
|
|
41
|
-
options?: LayoutOptions,
|
|
42
|
-
): ActivityChartLayout {
|
|
43
|
-
const width = options?.width ?? 960;
|
|
44
|
-
const left = options?.left ?? 56;
|
|
45
|
-
const right = options?.right ?? 18;
|
|
46
|
-
const top = options?.top ?? 24;
|
|
47
|
-
const bottom = options?.bottom ?? 36;
|
|
48
|
-
return {
|
|
49
|
-
bottom,
|
|
50
|
-
height,
|
|
51
|
-
left,
|
|
52
|
-
plotHeight: height - top - bottom,
|
|
53
|
-
plotWidth: width - left - right,
|
|
54
|
-
right,
|
|
55
|
-
top,
|
|
56
|
-
width,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
19
|
/** True when the bucket key is a UTC hour (`YYYY-MM-DDTHH`). */
|
|
61
20
|
export function isActivityHourBucket(date: string): boolean {
|
|
62
21
|
return /^\d{4}-\d{2}-\d{2}T\d{2}$/.test(date);
|
|
@@ -121,39 +80,11 @@ export function formatActivityCategoryTooltipLabel(label: string): string {
|
|
|
121
80
|
}
|
|
122
81
|
|
|
123
82
|
/** Choose the first, middle, and last available chart labels. */
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const chartAxisFontSizePx = 11;
|
|
131
|
-
const ChartSvgScaleContext = createContext(1);
|
|
132
|
-
|
|
133
|
-
/** Shared visual style for SVG chart axis labels. */
|
|
134
|
-
export const chartAxisLabelClassName =
|
|
135
|
-
"fill-dashboard-text-muted font-sans tabular-nums leading-none text-dashboard-text-muted";
|
|
136
|
-
|
|
137
|
-
type ChartAxisLabelProps = Omit<
|
|
138
|
-
SVGProps<SVGTextElement>,
|
|
139
|
-
"children" | "className" | "fill" | "fontFamily" | "fontSize"
|
|
140
|
-
> & {
|
|
141
|
-
children: ReactNode;
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
/** Render an SVG axis label at the shared on-screen size. */
|
|
145
|
-
export function ChartAxisLabel(props: ChartAxisLabelProps) {
|
|
146
|
-
const { children, ...textProps } = props;
|
|
147
|
-
const scale = useContext(ChartSvgScaleContext);
|
|
148
|
-
return (
|
|
149
|
-
<text
|
|
150
|
-
className={chartAxisLabelClassName}
|
|
151
|
-
fontSize={chartAxisFontSizePx / scale}
|
|
152
|
-
{...textProps}
|
|
153
|
-
>
|
|
154
|
-
{children}
|
|
155
|
-
</text>
|
|
156
|
-
);
|
|
83
|
+
function activityLabelIndexes(count: number): number[] {
|
|
84
|
+
if (count === 0) {
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
return [...new Set([0, Math.floor((count - 1) / 2), count - 1])];
|
|
157
88
|
}
|
|
158
89
|
|
|
159
90
|
/** Shared HTML axis label for non-SVG chart axes. */
|
|
@@ -175,151 +106,76 @@ export function ChartAxisHtmlLabel(props: {
|
|
|
175
106
|
);
|
|
176
107
|
}
|
|
177
108
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Screen pixels per SVG user unit. The default `preserveAspectRatio` fits the
|
|
182
|
-
* whole viewBox inside the element box, so the smaller ratio wins.
|
|
183
|
-
*/
|
|
184
|
-
function chartSvgScale(box: ChartBox | undefined, layout: ActivityChartLayout) {
|
|
185
|
-
if (!box || box.width <= 0 || box.height <= 0) return 1;
|
|
186
|
-
return Math.min(box.width / layout.width, box.height / layout.height);
|
|
187
|
-
}
|
|
109
|
+
/** Plot margins shared by dashboard Recharts charts. */
|
|
110
|
+
export const activityChartMargin = { bottom: 0, left: 4, right: 12, top: 12 };
|
|
188
111
|
|
|
189
|
-
/**
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
"aria-label": string;
|
|
197
|
-
children: ReactNode;
|
|
198
|
-
className?: string;
|
|
199
|
-
layout: ActivityChartLayout;
|
|
112
|
+
/** Render the first, middle, and last bucket labels under a Recharts chart. */
|
|
113
|
+
export function ActivityChartXAxis(props: {
|
|
114
|
+
/** Center every label, for categories that are not a time series. */
|
|
115
|
+
centered?: boolean;
|
|
116
|
+
dataKey?: string;
|
|
117
|
+
formatLabel?(label: string): string;
|
|
118
|
+
keys: readonly string[];
|
|
200
119
|
}) {
|
|
201
|
-
const
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const updateBox = (next: ChartBox) =>
|
|
208
|
-
setBox((previous) =>
|
|
209
|
-
previous?.width === next.width && previous.height === next.height
|
|
210
|
-
? previous
|
|
211
|
-
: next,
|
|
212
|
-
);
|
|
213
|
-
// Measure before the first paint; the observer reports later changes.
|
|
214
|
-
updateBox({ height: svg.clientHeight, width: svg.clientWidth });
|
|
215
|
-
const observer = new ResizeObserver(([entry]) => {
|
|
216
|
-
if (entry) updateBox(entry.contentRect);
|
|
217
|
-
});
|
|
218
|
-
observer.observe(svg);
|
|
219
|
-
return () => observer.disconnect();
|
|
220
|
-
}, []);
|
|
221
|
-
|
|
222
|
-
const scale = chartSvgScale(box, props.layout);
|
|
223
|
-
|
|
120
|
+
const formatLabel = props.formatLabel ?? formatActivityCategoryLabel;
|
|
121
|
+
const ticks = activityLabelIndexes(props.keys.length).map(
|
|
122
|
+
(index) => props.keys[index]!,
|
|
123
|
+
);
|
|
124
|
+
const first = props.keys[0];
|
|
125
|
+
const last = props.keys.at(-1);
|
|
224
126
|
return (
|
|
225
|
-
<
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
127
|
+
<XAxis
|
|
128
|
+
axisLine={false}
|
|
129
|
+
dataKey={props.dataKey ?? "date"}
|
|
130
|
+
interval={0}
|
|
131
|
+
tick={({ payload, x, y }) => {
|
|
132
|
+
const value = String(payload.value);
|
|
133
|
+
const anchor = props.centered
|
|
134
|
+
? "middle"
|
|
135
|
+
: value === first
|
|
136
|
+
? "start"
|
|
137
|
+
: value === last
|
|
138
|
+
? "end"
|
|
139
|
+
: "middle";
|
|
140
|
+
return (
|
|
141
|
+
<text
|
|
142
|
+
className="fill-dashboard-text-muted"
|
|
143
|
+
dy="0.71em"
|
|
144
|
+
textAnchor={anchor}
|
|
145
|
+
x={x}
|
|
146
|
+
y={y}
|
|
147
|
+
>
|
|
148
|
+
{formatLabel(value)}
|
|
149
|
+
</text>
|
|
150
|
+
);
|
|
151
|
+
}}
|
|
152
|
+
tickLine={false}
|
|
153
|
+
tickMargin={8}
|
|
154
|
+
ticks={ticks}
|
|
155
|
+
/>
|
|
236
156
|
);
|
|
237
157
|
}
|
|
238
158
|
|
|
239
|
-
/** Render
|
|
240
|
-
export function
|
|
159
|
+
/** Render minimum, middle, and maximum guide values on a Recharts chart. */
|
|
160
|
+
export function ActivityChartYAxis(props: {
|
|
241
161
|
format?(value: number): string;
|
|
242
|
-
layout: ActivityChartLayout;
|
|
243
162
|
maximum: number;
|
|
163
|
+
minimum?: number;
|
|
244
164
|
}) {
|
|
165
|
+
const minimum = props.minimum ?? 0;
|
|
245
166
|
const format = props.format ?? ((value: number) => String(Math.round(value)));
|
|
246
|
-
return
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
<ChartAxisLabel
|
|
259
|
-
textAnchor="end"
|
|
260
|
-
x={props.layout.left - 8}
|
|
261
|
-
y={yPosition + 3}
|
|
262
|
-
>
|
|
263
|
-
{format(props.maximum * (1 - ratio))}
|
|
264
|
-
</ChartAxisLabel>
|
|
265
|
-
</g>
|
|
266
|
-
);
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
/** Render the shared first, middle, and last date labels for activity charts. */
|
|
271
|
-
export function ActivityChartDateLabels(props: {
|
|
272
|
-
dates: readonly string[];
|
|
273
|
-
formatDate?(date: string): string;
|
|
274
|
-
layout: ActivityChartLayout;
|
|
275
|
-
xPosition(index: number): number;
|
|
276
|
-
}) {
|
|
277
|
-
const formatDate = props.formatDate ?? formatActivityDate;
|
|
278
|
-
return activityLabelIndexes(props.dates.length).map((index) => {
|
|
279
|
-
const date = props.dates[index];
|
|
280
|
-
if (!date) return null;
|
|
281
|
-
return (
|
|
282
|
-
<ChartAxisLabel
|
|
283
|
-
key={date}
|
|
284
|
-
textAnchor={
|
|
285
|
-
index === 0
|
|
286
|
-
? "start"
|
|
287
|
-
: index === props.dates.length - 1
|
|
288
|
-
? "end"
|
|
289
|
-
: "middle"
|
|
290
|
-
}
|
|
291
|
-
x={props.xPosition(index)}
|
|
292
|
-
y={props.layout.height - 8}
|
|
293
|
-
>
|
|
294
|
-
{formatDate(date)}
|
|
295
|
-
</ChartAxisLabel>
|
|
296
|
-
);
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
/** Render centered categorical labels using stable category keys. */
|
|
301
|
-
export function ChartCategoryLabels(props: {
|
|
302
|
-
categories: ReadonlyArray<{ id: string; label: string }>;
|
|
303
|
-
formatLabel?(label: string): string;
|
|
304
|
-
layout: ActivityChartLayout;
|
|
305
|
-
xPosition(index: number): number;
|
|
306
|
-
}) {
|
|
307
|
-
// Default formats day/hour bucket keys; leave other category labels alone.
|
|
308
|
-
const formatLabel = props.formatLabel ?? formatActivityCategoryLabel;
|
|
309
|
-
return activityLabelIndexes(props.categories.length).map((index) => {
|
|
310
|
-
const category = props.categories[index];
|
|
311
|
-
if (!category) return null;
|
|
312
|
-
return (
|
|
313
|
-
<ChartAxisLabel
|
|
314
|
-
key={category.id}
|
|
315
|
-
textAnchor="middle"
|
|
316
|
-
x={props.xPosition(index)}
|
|
317
|
-
y={props.layout.height - 8}
|
|
318
|
-
>
|
|
319
|
-
{formatLabel(category.label)}
|
|
320
|
-
</ChartAxisLabel>
|
|
321
|
-
);
|
|
322
|
-
});
|
|
167
|
+
return (
|
|
168
|
+
<YAxis
|
|
169
|
+
axisLine={false}
|
|
170
|
+
domain={[minimum, props.maximum]}
|
|
171
|
+
interval={0}
|
|
172
|
+
tickFormatter={format}
|
|
173
|
+
tickLine={false}
|
|
174
|
+
tickMargin={8}
|
|
175
|
+
ticks={[minimum, (minimum + props.maximum) / 2, props.maximum]}
|
|
176
|
+
width="auto"
|
|
177
|
+
/>
|
|
178
|
+
);
|
|
323
179
|
}
|
|
324
180
|
|
|
325
181
|
/** Mean of chart bucket values; empty input is 0. */
|
|
@@ -328,92 +184,122 @@ export function activityChartAverage(values: readonly number[]): number {
|
|
|
328
184
|
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
329
185
|
}
|
|
330
186
|
|
|
331
|
-
/** Shared visual style for average-line labels. */
|
|
332
|
-
export const chartAverageLabelClassName =
|
|
333
|
-
"fill-dashboard-text-solid font-sans tabular-nums leading-none";
|
|
334
|
-
|
|
335
187
|
/**
|
|
336
|
-
*
|
|
337
|
-
*
|
|
338
|
-
* Label sits on a dark chip so series-colored bars and guides stay off the text.
|
|
188
|
+
* Label for an average line, such as `3.2 / day`.
|
|
189
|
+
* Returns nothing for a zero mean or when the formatter floors it to zero.
|
|
339
190
|
*/
|
|
340
|
-
export function
|
|
341
|
-
average: number
|
|
342
|
-
format(value: number)
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const unit = props.unit ?? "day";
|
|
352
|
-
const formatted = props.format(props.average).trim();
|
|
353
|
-
// Flooring formatters can turn a real sub-1 mean into "0"; hide that case.
|
|
354
|
-
if (!formatted || /^\$?0(?:\.0+)?(?:ms)?$/i.test(formatted)) return null;
|
|
191
|
+
export function activityChartAverageLabel(
|
|
192
|
+
average: number,
|
|
193
|
+
format: (value: number) => string,
|
|
194
|
+
unit = "day",
|
|
195
|
+
): string | undefined {
|
|
196
|
+
if (!(average > 0)) return undefined;
|
|
197
|
+
const formatted = format(average).trim();
|
|
198
|
+
if (!formatted || /^\$?0(?:\.0+)?(?:ms)?$/i.test(formatted)) return undefined;
|
|
199
|
+
return `${formatted} / ${unit}`;
|
|
200
|
+
}
|
|
355
201
|
|
|
356
|
-
|
|
357
|
-
props.layout.top +
|
|
358
|
-
props.layout.plotHeight -
|
|
359
|
-
(props.average / props.maximum) * props.layout.plotHeight;
|
|
360
|
-
const x1 = props.layout.left;
|
|
361
|
-
const x2 = props.layout.width - props.layout.right;
|
|
362
|
-
const label = `${formatted} / ${unit}`;
|
|
363
|
-
const fontSize = chartAxisFontSizePx / scale;
|
|
364
|
-
// Mono width estimate for the chip and dashed-guide stop.
|
|
365
|
-
const labelWidth = Math.max(52, label.length * fontSize * 0.68);
|
|
366
|
-
const chipPadX = 5;
|
|
367
|
-
const chipPadY = 3;
|
|
368
|
-
const chipHeight = fontSize + chipPadY * 2;
|
|
369
|
-
const chipWidth = labelWidth + chipPadX * 2;
|
|
370
|
-
const chipX = x2 - chipWidth;
|
|
371
|
-
// Keep the chip inside the plot; flip below the line near the top edge.
|
|
372
|
-
const labelAbove = y - props.layout.top > chipHeight + 4;
|
|
373
|
-
const chipY = labelAbove ? y - chipHeight - 3 : y + 4;
|
|
374
|
-
const textY = chipY + chipHeight / 2 + fontSize * 0.35;
|
|
375
|
-
const lineEnd = Math.max(x1, chipX - 6);
|
|
202
|
+
const AVERAGE_LABEL_FONT_PX = 12;
|
|
376
203
|
|
|
204
|
+
/**
|
|
205
|
+
* Draw the average label on a solid chip at the right end of the line, so
|
|
206
|
+
* series colors stay off the text. The chip flips below the line near the top.
|
|
207
|
+
*/
|
|
208
|
+
function ActivityChartAverageChip(props: {
|
|
209
|
+
text: string;
|
|
210
|
+
viewBox?: { width?: number; x?: number; y?: number };
|
|
211
|
+
}) {
|
|
212
|
+
const plotArea = usePlotArea();
|
|
213
|
+
const lineY = props.viewBox?.y ?? 0;
|
|
214
|
+
const right = (props.viewBox?.x ?? 0) + (props.viewBox?.width ?? 0);
|
|
215
|
+
const chipHeight = AVERAGE_LABEL_FONT_PX + 6;
|
|
216
|
+
// Width estimate for tabular digits at the label size.
|
|
217
|
+
const chipWidth =
|
|
218
|
+
Math.max(52, props.text.length * AVERAGE_LABEL_FONT_PX * 0.62) + 10;
|
|
219
|
+
const above = lineY - (plotArea?.y ?? 0) > chipHeight + 4;
|
|
220
|
+
const chipY = above ? lineY - chipHeight - 3 : lineY + 4;
|
|
377
221
|
return (
|
|
378
|
-
<g
|
|
379
|
-
<line
|
|
380
|
-
stroke={props.stroke ?? "var(--color-dashboard-text-faint)"}
|
|
381
|
-
strokeDasharray="4 4"
|
|
382
|
-
strokeOpacity={0.85}
|
|
383
|
-
strokeWidth={1.25}
|
|
384
|
-
x1={x1}
|
|
385
|
-
x2={lineEnd}
|
|
386
|
-
y1={y}
|
|
387
|
-
y2={y}
|
|
388
|
-
/>
|
|
389
|
-
{/* Solid chip so series colors never wash out the label text. */}
|
|
222
|
+
<g pointerEvents="none">
|
|
390
223
|
<rect
|
|
391
224
|
fill="var(--color-dashboard-bg-elevated)"
|
|
392
225
|
fillOpacity={0.92}
|
|
393
226
|
height={chipHeight}
|
|
394
227
|
rx={3}
|
|
395
228
|
width={chipWidth}
|
|
396
|
-
x={
|
|
229
|
+
x={right - chipWidth}
|
|
397
230
|
y={chipY}
|
|
398
231
|
/>
|
|
399
232
|
<text
|
|
400
|
-
className=
|
|
401
|
-
|
|
402
|
-
fontSize={fontSize}
|
|
233
|
+
className="fill-dashboard-text-solid font-sans tabular-nums"
|
|
234
|
+
dominantBaseline="central"
|
|
403
235
|
textAnchor="middle"
|
|
404
|
-
x={
|
|
405
|
-
y={
|
|
236
|
+
x={right - chipWidth / 2}
|
|
237
|
+
y={chipY + chipHeight / 2}
|
|
406
238
|
>
|
|
407
|
-
{
|
|
239
|
+
{props.text}
|
|
408
240
|
</text>
|
|
409
241
|
</g>
|
|
410
242
|
);
|
|
411
243
|
}
|
|
412
244
|
|
|
413
|
-
/**
|
|
414
|
-
|
|
415
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Opt-in horizontal average across the plotted buckets.
|
|
247
|
+
* Renders a dashed guide plus a compact "value / day" label on the right.
|
|
248
|
+
*/
|
|
249
|
+
export function ActivityChartAverageLine(props: {
|
|
250
|
+
average: number;
|
|
251
|
+
format(value: number): string;
|
|
252
|
+
stroke?: string;
|
|
253
|
+
unit?: string;
|
|
416
254
|
}) {
|
|
255
|
+
const label = activityChartAverageLabel(
|
|
256
|
+
props.average,
|
|
257
|
+
props.format,
|
|
258
|
+
props.unit,
|
|
259
|
+
);
|
|
260
|
+
if (!label) return null;
|
|
261
|
+
return (
|
|
262
|
+
<ReferenceLine
|
|
263
|
+
aria-label={`average ${label}`}
|
|
264
|
+
ifOverflow="extendDomain"
|
|
265
|
+
label={<ActivityChartAverageChip text={label} />}
|
|
266
|
+
stroke={props.stroke ?? "var(--color-dashboard-text-faint)"}
|
|
267
|
+
strokeDasharray="4 4"
|
|
268
|
+
strokeOpacity={0.85}
|
|
269
|
+
strokeWidth={1.25}
|
|
270
|
+
y={props.average}
|
|
271
|
+
/>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
type TooltipRows = ReadonlyArray<readonly [label: ReactNode, value: ReactNode]>;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Recharts tooltip body for one activity bucket. It titles the bucket with
|
|
279
|
+
* `formatLabel`, or by default `formatActivityCategoryTooltipLabel`, and lists
|
|
280
|
+
* `rows` for the hovered datum.
|
|
281
|
+
* Pass it as `content` of `ChartTooltip`; Recharts injects the other props.
|
|
282
|
+
*/
|
|
283
|
+
export function ActivityChartTooltipContent<Datum>(props: {
|
|
284
|
+
active?: boolean;
|
|
285
|
+
formatLabel?(label: string): string;
|
|
286
|
+
label?: unknown;
|
|
287
|
+
payload?: ReadonlyArray<{ payload?: unknown }>;
|
|
288
|
+
rows(datum: Datum): TooltipRows;
|
|
289
|
+
}) {
|
|
290
|
+
const datum = props.payload?.[0]?.payload as Datum | undefined;
|
|
291
|
+
if (!props.active || datum === undefined) return null;
|
|
292
|
+
const formatLabel = props.formatLabel ?? formatActivityCategoryTooltipLabel;
|
|
293
|
+
return (
|
|
294
|
+
<div className={tooltipContentVariants()} role="status">
|
|
295
|
+
<TooltipTitle>{formatLabel(String(props.label))}</TooltipTitle>
|
|
296
|
+
<ActivityTooltipRows rows={props.rows(datum)} />
|
|
297
|
+
</div>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Render label and value rows inside an activity-chart tooltip. */
|
|
302
|
+
export function ActivityTooltipRows(props: { rows: TooltipRows }) {
|
|
417
303
|
return (
|
|
418
304
|
<div className="grid grid-cols-[auto_auto] gap-x-4 gap-y-0.5">
|
|
419
305
|
{props.rows.flatMap(([label, value], index) => [
|
|
@@ -439,7 +325,6 @@ export function ActivityChartTooltip(props: {
|
|
|
439
325
|
content: ReactNode;
|
|
440
326
|
date: string;
|
|
441
327
|
summary?: string;
|
|
442
|
-
triggerClassName?: string;
|
|
443
328
|
}) {
|
|
444
329
|
const dateLabel = formatActivityTooltipDate(props.date);
|
|
445
330
|
const child = props.summary
|
|
@@ -449,12 +334,12 @@ export function ActivityChartTooltip(props: {
|
|
|
449
334
|
: props.children;
|
|
450
335
|
|
|
451
336
|
return (
|
|
452
|
-
<Tooltip
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
337
|
+
<Tooltip>
|
|
338
|
+
<TooltipTrigger render={child} />
|
|
339
|
+
<TooltipContent>
|
|
340
|
+
<TooltipTitle>{dateLabel}</TooltipTitle>
|
|
341
|
+
{props.content}
|
|
342
|
+
</TooltipContent>
|
|
458
343
|
</Tooltip>
|
|
459
344
|
);
|
|
460
345
|
}
|