@sentry/junior-dashboard 0.259.0 → 0.261.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/README.md +5 -3
- package/dist/app.js +88 -37
- package/dist/chunks/github-light-QHW3AEMR.js +1 -0
- package/dist/client.js +86 -50
- package/dist/index.js +88 -37
- package/dist/tailwind.css +1 -1
- package/package.json +6 -6
- package/src/client/App.tsx +0 -1
- package/src/client/code.tsx +10 -4
- package/src/client/components/ProfileMenu.tsx +3 -0
- package/src/client/components/README.md +14 -3
- package/src/client/components/Select.tsx +41 -7
- package/src/client/components/ThemeSwitcher.tsx +48 -0
- package/src/client/components/charts/ActivityChart.tsx +6 -7
- package/src/client/components/charts/InputCacheChart.tsx +1 -1
- package/src/client/components/charts/SystemMetricCharts.tsx +3 -3
- package/src/client/components/charts/WorkspaceUsageChart.tsx +1 -1
- package/src/client/components/charts/cache-input.ts +11 -3
- package/src/client/components/controls/DirectorySortSelect.tsx +6 -4
- package/src/client/components/layout/DashboardHeader.tsx +2 -9
- package/src/client/conversations/ConversationBrief.tsx +35 -9
- package/src/client/conversations/ConversationHomeList.tsx +7 -1
- package/src/client/conversations/ConversationMeta.tsx +19 -7
- package/src/client/conversations/ConversationTranscript.tsx +8 -11
- package/src/client/conversations/ConversationWorkspace.tsx +9 -12
- package/src/client/conversations/ConversationWorkspaceLoading.tsx +1 -4
- package/src/client/conversations/EventDetails.tsx +9 -5
- package/src/client/conversations/README.md +6 -0
- package/src/client/conversations/TranscriptContextEventView.tsx +5 -1
- package/src/client/conversations/TranscriptEventView.tsx +48 -9
- package/src/client/conversations/TranscriptMessageView.tsx +6 -1
- package/src/client/conversations/TranscriptStructuredEventView.tsx +4 -2
- package/src/client/conversations/TranscriptText.tsx +28 -20
- package/src/client/conversations/conversationActivityPreview.ts +6 -0
- package/src/client/conversations/eventTranscript.ts +4 -1
- package/src/client/conversations/transcriptMentions.tsx +27 -10
- package/src/client/debouncedValue.ts +14 -0
- package/src/client/pages/automations/AutomationCostChart.tsx +2 -2
- package/src/client/pages/automations/AutomationDetailsDrawer.tsx +4 -1
- package/src/client/pages/automations/AutomationExecutionChart.tsx +2 -2
- package/src/client/pages/automations/AutomationExecutionStatusChart.tsx +3 -3
- package/src/client/pages/automations/AutomationFilters.tsx +4 -3
- package/src/client/pages/code/CodeActivityChart.tsx +4 -4
- package/src/client/pages/code/RepositoryTabs.tsx +3 -2
- package/src/client/pages/dev/ComponentsPage.tsx +11 -3
- package/src/client/pages/dev/ConversationFixture.tsx +29 -0
- package/src/client/pages/dev/SelectFixtures.tsx +62 -0
- package/src/client/pages/locations/LocationActivityChart.tsx +14 -4
- package/src/client/pages/locations/LocationDirectoryActivityChart.tsx +14 -9
- package/src/client/pages/memory/MemoryCostChart.tsx +5 -5
- package/src/client/pages/memory/MemoryDetailsDrawer.tsx +9 -2
- package/src/client/pages/memory/MemoryTimeline.tsx +2 -2
- package/src/client/pages/people/ContributionGrid.tsx +8 -8
- package/src/client/pages/people/PeopleActivityChart.tsx +23 -10
- package/src/client/pages/system/BaseSandboxCard.tsx +76 -0
- package/src/client/pages/system/ConversationActivityChart.tsx +2 -2
- package/src/client/pages/system/PluginBarChart.tsx +12 -6
- package/src/client/pages/system/SandboxSizeSummary.tsx +47 -0
- package/src/client/pages/system/WorkspaceDetails.tsx +11 -1
- package/src/client/pages/system/WorkspaceEditor.tsx +19 -0
- package/src/client/pages/system/WorkspaceFormPage.tsx +28 -19
- package/src/client/pages/system/WorkspaceList.tsx +6 -1
- package/src/client/pages/system/WorkspacesPage.tsx +18 -12
- package/src/client/pages/system/workspaceDraft.ts +8 -0
- package/src/client/theme.ts +52 -0
- package/src/client/types.ts +8 -1
- package/src/mock-reporting/fixtures.ts +48 -35
- package/src/shell.ts +4 -1
- package/src/tailwind.css +109 -0
- package/src/theme.ts +49 -0
- package/src/client/pages/system/BaselineSnapshotCard.tsx +0 -18
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { DirectorySortSelect } from "../../components/controls/DirectorySortSelect";
|
|
3
|
+
import { Field } from "../../components/Field";
|
|
4
|
+
import { FilterGroup } from "../../components/FilterBar";
|
|
5
|
+
import { Select } from "../../components/Select";
|
|
6
|
+
|
|
7
|
+
/** Show shared select arrows at form, filter, and directory sizes. */
|
|
8
|
+
export function SelectFixtures() {
|
|
9
|
+
const [sort, setSort] = useState("recent");
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className="grid min-w-0 gap-4">
|
|
13
|
+
<Field label="Schedule" htmlFor="gallery-select-form">
|
|
14
|
+
<Select id="gallery-select-form" defaultValue="weekly">
|
|
15
|
+
<option value="daily">Every day</option>
|
|
16
|
+
<option value="weekly">Every week</option>
|
|
17
|
+
</Select>
|
|
18
|
+
</Field>
|
|
19
|
+
<FilterGroup label="State">
|
|
20
|
+
<Select aria-label="State" variant="filter">
|
|
21
|
+
<option>All states</option>
|
|
22
|
+
<option>Active</option>
|
|
23
|
+
<option>Paused</option>
|
|
24
|
+
</Select>
|
|
25
|
+
</FilterGroup>
|
|
26
|
+
<DirectorySortSelect
|
|
27
|
+
ariaLabel="Gallery sort"
|
|
28
|
+
onChange={setSort}
|
|
29
|
+
options={[
|
|
30
|
+
{ label: "Recently created", value: "recent" },
|
|
31
|
+
{ label: "Oldest created", value: "oldest" },
|
|
32
|
+
]}
|
|
33
|
+
value={sort}
|
|
34
|
+
/>
|
|
35
|
+
<FilterGroup className="w-36" label="Opened">
|
|
36
|
+
<Select aria-label="Opened">
|
|
37
|
+
<option>Last 90 days</option>
|
|
38
|
+
<option>Any time</option>
|
|
39
|
+
</Select>
|
|
40
|
+
</FilterGroup>
|
|
41
|
+
<Field label="Disabled" htmlFor="gallery-select-disabled">
|
|
42
|
+
<Select id="gallery-select-disabled" disabled>
|
|
43
|
+
<option>Every week</option>
|
|
44
|
+
</Select>
|
|
45
|
+
</Field>
|
|
46
|
+
<Field
|
|
47
|
+
label="Destination"
|
|
48
|
+
htmlFor="gallery-select-invalid"
|
|
49
|
+
error="Choose a destination."
|
|
50
|
+
>
|
|
51
|
+
<Select
|
|
52
|
+
id="gallery-select-invalid"
|
|
53
|
+
aria-invalid
|
|
54
|
+
aria-describedby="gallery-select-invalid-error"
|
|
55
|
+
>
|
|
56
|
+
<option value="">Choose a destination</option>
|
|
57
|
+
<option value="conversation">This Conversation</option>
|
|
58
|
+
</Select>
|
|
59
|
+
</Field>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -43,7 +43,13 @@ export function LocationActivityChart(props: {
|
|
|
43
43
|
<ChartLegend
|
|
44
44
|
ariaLabel="Conversation activity period"
|
|
45
45
|
inline
|
|
46
|
-
items={[
|
|
46
|
+
items={[
|
|
47
|
+
{
|
|
48
|
+
color: "var(--color-cyan-400)",
|
|
49
|
+
key: "period",
|
|
50
|
+
label: "90 days",
|
|
51
|
+
},
|
|
52
|
+
]}
|
|
47
53
|
/>
|
|
48
54
|
}
|
|
49
55
|
/>
|
|
@@ -55,8 +61,12 @@ export function LocationActivityChart(props: {
|
|
|
55
61
|
>
|
|
56
62
|
<defs>
|
|
57
63
|
<linearGradient id="location-bars" x1="0" x2="0" y1="0" y2="1">
|
|
58
|
-
<stop offset="0%" stopColor="
|
|
59
|
-
<stop
|
|
64
|
+
<stop offset="0%" stopColor="var(--color-cyan-400)" />
|
|
65
|
+
<stop
|
|
66
|
+
offset="100%"
|
|
67
|
+
stopColor="var(--color-amber-500)"
|
|
68
|
+
stopOpacity="0.65"
|
|
69
|
+
/>
|
|
60
70
|
</linearGradient>
|
|
61
71
|
</defs>
|
|
62
72
|
<ActivityChartGrid layout={layout} maximum={maximum} />
|
|
@@ -98,7 +108,7 @@ export function LocationActivityChart(props: {
|
|
|
98
108
|
format={formatActivityChartAverage}
|
|
99
109
|
layout={layout}
|
|
100
110
|
maximum={maximum}
|
|
101
|
-
stroke="
|
|
111
|
+
stroke="var(--color-cyan-400)"
|
|
102
112
|
/>
|
|
103
113
|
<ActivityChartDateLabels
|
|
104
114
|
dates={props.days.map((day) => day.date)}
|
|
@@ -24,8 +24,7 @@ export function LocationDirectoryActivityChart(props: {
|
|
|
24
24
|
days: LocationActivityDayReport[];
|
|
25
25
|
}) {
|
|
26
26
|
const bucketUnit = props.bucketUnit ?? "day";
|
|
27
|
-
const perBucket =
|
|
28
|
-
`Conversations per ${timeRangeBucketPerLabel(bucketUnit)}`;
|
|
27
|
+
const perBucket = `Conversations per ${timeRangeBucketPerLabel(bucketUnit)}`;
|
|
29
28
|
|
|
30
29
|
const layout = createActivityChartLayout(260);
|
|
31
30
|
const maximum = Math.max(
|
|
@@ -42,17 +41,23 @@ export function LocationDirectoryActivityChart(props: {
|
|
|
42
41
|
return (
|
|
43
42
|
<Card>
|
|
44
43
|
<CardHeader
|
|
45
|
-
description={
|
|
46
|
-
`${timeRangeBucketAdjective(bucketUnit)} public volume compared with private activity in aggregate.`
|
|
47
|
-
}
|
|
44
|
+
description={`${timeRangeBucketAdjective(bucketUnit)} public volume compared with private activity in aggregate.`}
|
|
48
45
|
title={perBucket}
|
|
49
46
|
trailing={
|
|
50
47
|
<ChartLegend
|
|
51
48
|
ariaLabel="Conversation visibility legend"
|
|
52
49
|
inline
|
|
53
50
|
items={[
|
|
54
|
-
{
|
|
55
|
-
|
|
51
|
+
{
|
|
52
|
+
color: "var(--color-cyan-400)",
|
|
53
|
+
key: "public",
|
|
54
|
+
label: "public",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
color: "var(--color-amber-400)",
|
|
58
|
+
key: "private",
|
|
59
|
+
label: "private",
|
|
60
|
+
},
|
|
56
61
|
]}
|
|
57
62
|
/>
|
|
58
63
|
}
|
|
@@ -70,13 +75,13 @@ export function LocationDirectoryActivityChart(props: {
|
|
|
70
75
|
[
|
|
71
76
|
{
|
|
72
77
|
count: day.publicConversations,
|
|
73
|
-
fill: "
|
|
78
|
+
fill: "var(--color-cyan-400)",
|
|
74
79
|
key: "public",
|
|
75
80
|
x: groupX,
|
|
76
81
|
},
|
|
77
82
|
{
|
|
78
83
|
count: day.privateConversations,
|
|
79
|
-
fill: "
|
|
84
|
+
fill: "var(--color-amber-400)",
|
|
80
85
|
key: "private",
|
|
81
86
|
x: groupX + barWidth + gap,
|
|
82
87
|
},
|
|
@@ -74,13 +74,13 @@ export function MemoryCostChart(props: {
|
|
|
74
74
|
ariaLabel="Memory cost legend"
|
|
75
75
|
items={[
|
|
76
76
|
{
|
|
77
|
-
color: "
|
|
77
|
+
color: "var(--color-cyan-300)",
|
|
78
78
|
key: "extraction",
|
|
79
79
|
label: "Extraction",
|
|
80
80
|
value: formatCostSummary({ total: extractionTotal }),
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
|
-
color: "
|
|
83
|
+
color: "var(--color-fuchsia-400)",
|
|
84
84
|
key: "recall",
|
|
85
85
|
label: "Recall",
|
|
86
86
|
value: formatCostSummary({ total: recallTotal }),
|
|
@@ -130,7 +130,7 @@ export function MemoryCostChart(props: {
|
|
|
130
130
|
>
|
|
131
131
|
<g tabIndex={0}>
|
|
132
132
|
<rect
|
|
133
|
-
fill="
|
|
133
|
+
fill="var(--color-cyan-300)"
|
|
134
134
|
height={extractionHeight}
|
|
135
135
|
opacity={day.extraction.costUsd > 0 ? 0.82 : 0.1}
|
|
136
136
|
rx="1"
|
|
@@ -139,7 +139,7 @@ export function MemoryCostChart(props: {
|
|
|
139
139
|
y={layout.top + layout.plotHeight - extractionHeight}
|
|
140
140
|
/>
|
|
141
141
|
<rect
|
|
142
|
-
fill="
|
|
142
|
+
fill="var(--color-fuchsia-400)"
|
|
143
143
|
height={recallHeight}
|
|
144
144
|
opacity={day.recall.costUsd > 0 ? 0.82 : 0.1}
|
|
145
145
|
rx="1"
|
|
@@ -169,7 +169,7 @@ export function MemoryCostChart(props: {
|
|
|
169
169
|
format={(value) => formatCostSummary({ total: value })}
|
|
170
170
|
layout={layout}
|
|
171
171
|
maximum={maximum}
|
|
172
|
-
stroke="
|
|
172
|
+
stroke="var(--color-slate-200)"
|
|
173
173
|
/>
|
|
174
174
|
<ActivityChartDateLabels
|
|
175
175
|
dates={days.map((day) => day.date)}
|
|
@@ -10,6 +10,10 @@ import type {
|
|
|
10
10
|
PluginUserPageRecordAction,
|
|
11
11
|
} from "../user/pluginUserPageData";
|
|
12
12
|
|
|
13
|
+
// The memory plugin API does not resolve mentions yet, so memory text shows
|
|
14
|
+
// Slack ids instead of names.
|
|
15
|
+
const NO_MEMORY_MENTIONS: readonly [] = [];
|
|
16
|
+
|
|
13
17
|
type MemoryActionMutation = UseMutationResult<
|
|
14
18
|
void,
|
|
15
19
|
Error,
|
|
@@ -86,10 +90,13 @@ export function MemoryDetailsDrawer(props: {
|
|
|
86
90
|
<div className="mb-2 font-sans font-medium text-xs text-dashboard-text-muted">
|
|
87
91
|
Memory
|
|
88
92
|
</div>
|
|
89
|
-
<TranscriptText text={record.title} />
|
|
93
|
+
<TranscriptText mentions={NO_MEMORY_MENTIONS} text={record.title} />
|
|
90
94
|
{record.description ? (
|
|
91
95
|
<div className="mt-3">
|
|
92
|
-
<TranscriptText
|
|
96
|
+
<TranscriptText
|
|
97
|
+
mentions={NO_MEMORY_MENTIONS}
|
|
98
|
+
text={record.description}
|
|
99
|
+
/>
|
|
93
100
|
</div>
|
|
94
101
|
) : null}
|
|
95
102
|
</div>
|
|
@@ -15,8 +15,8 @@ import { Card } from "../../components/layout/Card";
|
|
|
15
15
|
import type { MemoryDay } from "./memoryDashboard";
|
|
16
16
|
|
|
17
17
|
const series = [
|
|
18
|
-
{ color: "
|
|
19
|
-
{ color: "
|
|
18
|
+
{ color: "var(--color-cyan-300)", key: "personal", label: "Personal" },
|
|
19
|
+
{ color: "var(--color-emerald-300)", key: "public", label: "Public" },
|
|
20
20
|
] as const;
|
|
21
21
|
|
|
22
22
|
/** Render viewer memory creation as a stacked personal/public timeline. */
|
|
@@ -124,10 +124,10 @@ export function ContributionGrid(props: { days: ContributionDay[] }) {
|
|
|
124
124
|
className={cn(
|
|
125
125
|
"size-3 border border-dashboard-overlay",
|
|
126
126
|
level === 0 && "bg-dashboard-surface-hover",
|
|
127
|
-
level === 1 && "bg-[#133225]",
|
|
128
|
-
level === 2 && "bg-[#176a4a]",
|
|
129
|
-
level === 3 && "bg-[#22a06b]",
|
|
130
|
-
level === 4 && "bg-[#8bdc97]",
|
|
127
|
+
level === 1 && "bg-[#133225] light:bg-[#9be9a8]",
|
|
128
|
+
level === 2 && "bg-[#176a4a] light:bg-[#40c463]",
|
|
129
|
+
level === 3 && "bg-[#22a06b] light:bg-[#30a14e]",
|
|
130
|
+
level === 4 && "bg-[#8bdc97] light:bg-[#216e39]",
|
|
131
131
|
)}
|
|
132
132
|
key={level}
|
|
133
133
|
/>
|
|
@@ -156,8 +156,8 @@ function activityRuntime(day: ContributionDay): string {
|
|
|
156
156
|
function activityClass(count: number, max: number): string {
|
|
157
157
|
if (count <= 0) return "bg-dashboard-surface-hover";
|
|
158
158
|
const ratio = count / max;
|
|
159
|
-
if (ratio >= 0.75) return "bg-[#8bdc97]";
|
|
160
|
-
if (ratio >= 0.45) return "bg-[#22a06b]";
|
|
161
|
-
if (ratio >= 0.2) return "bg-[#176a4a]";
|
|
162
|
-
return "bg-[#133225]";
|
|
159
|
+
if (ratio >= 0.75) return "bg-[#8bdc97] light:bg-[#216e39]";
|
|
160
|
+
if (ratio >= 0.45) return "bg-[#22a06b] light:bg-[#30a14e]";
|
|
161
|
+
if (ratio >= 0.2) return "bg-[#176a4a] light:bg-[#40c463]";
|
|
162
|
+
return "bg-[#133225] light:bg-[#9be9a8]";
|
|
163
163
|
}
|
|
@@ -44,8 +44,7 @@ export function PeopleActivityChart(props: {
|
|
|
44
44
|
days: PeopleActivityDayReport[];
|
|
45
45
|
}) {
|
|
46
46
|
const bucketUnit = props.bucketUnit ?? "day";
|
|
47
|
-
const chartTitle =
|
|
48
|
-
`Active people per ${timeRangeBucketPerLabel(bucketUnit)}`;
|
|
47
|
+
const chartTitle = `Active people per ${timeRangeBucketPerLabel(bucketUnit)}`;
|
|
49
48
|
|
|
50
49
|
const layout = createActivityChartLayout(260);
|
|
51
50
|
const values = props.days.map((day) => day.activePeople);
|
|
@@ -70,7 +69,13 @@ export function PeopleActivityChart(props: {
|
|
|
70
69
|
<ChartLegend
|
|
71
70
|
ariaLabel="People activity legend"
|
|
72
71
|
inline
|
|
73
|
-
items={[
|
|
72
|
+
items={[
|
|
73
|
+
{
|
|
74
|
+
color: "var(--color-amber-400)",
|
|
75
|
+
key: "people",
|
|
76
|
+
label: "people",
|
|
77
|
+
},
|
|
78
|
+
]}
|
|
74
79
|
/>
|
|
75
80
|
}
|
|
76
81
|
/>
|
|
@@ -82,13 +87,21 @@ export function PeopleActivityChart(props: {
|
|
|
82
87
|
>
|
|
83
88
|
<defs>
|
|
84
89
|
<linearGradient id="people-area" x1="0" x2="0" y1="0" y2="1">
|
|
85
|
-
<stop
|
|
86
|
-
|
|
90
|
+
<stop
|
|
91
|
+
offset="0%"
|
|
92
|
+
stopColor="var(--color-amber-500)"
|
|
93
|
+
stopOpacity="0.3"
|
|
94
|
+
/>
|
|
95
|
+
<stop
|
|
96
|
+
offset="100%"
|
|
97
|
+
stopColor="var(--color-amber-500)"
|
|
98
|
+
stopOpacity="0"
|
|
99
|
+
/>
|
|
87
100
|
</linearGradient>
|
|
88
101
|
<linearGradient id="people-line" x1="0" x2="1" y1="0" y2="0">
|
|
89
|
-
<stop offset="0%" stopColor="
|
|
90
|
-
<stop offset="55%" stopColor="
|
|
91
|
-
<stop offset="100%" stopColor="
|
|
102
|
+
<stop offset="0%" stopColor="var(--color-amber-500)" />
|
|
103
|
+
<stop offset="55%" stopColor="var(--color-amber-400)" />
|
|
104
|
+
<stop offset="100%" stopColor="var(--color-cyan-400)" />
|
|
92
105
|
</linearGradient>
|
|
93
106
|
</defs>
|
|
94
107
|
<ActivityChartGrid layout={layout} maximum={maximum} />
|
|
@@ -122,7 +135,7 @@ export function PeopleActivityChart(props: {
|
|
|
122
135
|
<circle
|
|
123
136
|
cx={point.x}
|
|
124
137
|
cy={point.y}
|
|
125
|
-
fill="
|
|
138
|
+
fill="var(--color-amber-400)"
|
|
126
139
|
opacity={props.days.length > 30 ? 0.35 : 0.75}
|
|
127
140
|
r={props.days.length > 30 ? 3 : 4}
|
|
128
141
|
tabIndex={0}
|
|
@@ -136,7 +149,7 @@ export function PeopleActivityChart(props: {
|
|
|
136
149
|
format={formatActivityChartAverage}
|
|
137
150
|
layout={layout}
|
|
138
151
|
maximum={maximum}
|
|
139
|
-
stroke="
|
|
152
|
+
stroke="var(--color-amber-400)"
|
|
140
153
|
/>
|
|
141
154
|
<ActivityChartDateLabels
|
|
142
155
|
dates={props.days.map((day) => day.date)}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import type { BaselineSnapshotReport } from "@sentry/junior/api/schema";
|
|
3
|
+
|
|
4
|
+
import { Card } from "../../components/layout/Card";
|
|
5
|
+
import { formatMs, formatRelativeTime, formatTime } from "../../format";
|
|
6
|
+
import { formatSandboxSize } from "./SandboxSizeSummary";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Show the base Sandbox: the default size and the baseline snapshot.
|
|
10
|
+
* Workspaces inherit the base setup and use the default size unless they set
|
|
11
|
+
* vCPUs.
|
|
12
|
+
*/
|
|
13
|
+
export function BaseSandboxCard(props: {
|
|
14
|
+
defaultVcpus: number | null;
|
|
15
|
+
snapshot: BaselineSnapshotReport | null;
|
|
16
|
+
}) {
|
|
17
|
+
const snapshot = props.snapshot;
|
|
18
|
+
return (
|
|
19
|
+
<Card as="section" className="mb-0" padding="md" variant="section">
|
|
20
|
+
<div aria-labelledby="base-sandbox-heading" className="grid gap-4">
|
|
21
|
+
<div className="min-w-0 grid gap-1">
|
|
22
|
+
<h2
|
|
23
|
+
className="m-0 text-base font-semibold text-dashboard-text"
|
|
24
|
+
id="base-sandbox-heading"
|
|
25
|
+
>
|
|
26
|
+
Base Sandbox
|
|
27
|
+
</h2>
|
|
28
|
+
<p className="m-0 text-sm leading-relaxed text-dashboard-text-muted">
|
|
29
|
+
The base snapshot is inherited by all Workspaces.
|
|
30
|
+
</p>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<dl className="m-0 grid grid-cols-[max-content_minmax(0,1fr)] gap-x-6 gap-y-2 text-sm">
|
|
34
|
+
<Fact label="Size">{formatSandboxSize(props.defaultVcpus)}</Fact>
|
|
35
|
+
{snapshot ? (
|
|
36
|
+
<>
|
|
37
|
+
{snapshot.dependencyCount == null ? null : (
|
|
38
|
+
<Fact label="Dependencies">{snapshot.dependencyCount}</Fact>
|
|
39
|
+
)}
|
|
40
|
+
<Fact label="Build time">
|
|
41
|
+
{formatMs(snapshot.buildDurationMs)}
|
|
42
|
+
</Fact>
|
|
43
|
+
<Fact label="Generated">
|
|
44
|
+
<span title={formatTime(snapshot.generatedAt)}>
|
|
45
|
+
{formatRelativeTime(snapshot.generatedAt)}
|
|
46
|
+
</span>
|
|
47
|
+
</Fact>
|
|
48
|
+
<Fact label="Snapshot">
|
|
49
|
+
<span className="break-all font-mono text-xs">
|
|
50
|
+
{snapshot.id}
|
|
51
|
+
</span>
|
|
52
|
+
</Fact>
|
|
53
|
+
</>
|
|
54
|
+
) : (
|
|
55
|
+
<Fact label="Snapshot">
|
|
56
|
+
<span className="text-dashboard-text-muted">
|
|
57
|
+
Not built yet. The first Sandbox without a Workspace builds it.
|
|
58
|
+
</span>
|
|
59
|
+
</Fact>
|
|
60
|
+
)}
|
|
61
|
+
</dl>
|
|
62
|
+
</div>
|
|
63
|
+
</Card>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function Fact(props: { children: ReactNode; label: string }) {
|
|
68
|
+
return (
|
|
69
|
+
<div className="contents">
|
|
70
|
+
<dt className="text-dashboard-text-muted">{props.label}</dt>
|
|
71
|
+
<dd className="m-0 min-w-0 tabular-nums text-dashboard-text">
|
|
72
|
+
{props.children}
|
|
73
|
+
</dd>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -65,7 +65,7 @@ export function ConversationActivityChart(props: {
|
|
|
65
65
|
>
|
|
66
66
|
<g tabIndex={0}>
|
|
67
67
|
<rect
|
|
68
|
-
fill="
|
|
68
|
+
fill="var(--color-cyan-400)"
|
|
69
69
|
height={renderedHeight}
|
|
70
70
|
opacity={day.conversations ? 0.78 : 0.08}
|
|
71
71
|
rx="2"
|
|
@@ -89,7 +89,7 @@ export function ConversationActivityChart(props: {
|
|
|
89
89
|
format={formatActivityChartAverage}
|
|
90
90
|
layout={layout}
|
|
91
91
|
maximum={maximum}
|
|
92
|
-
stroke="
|
|
92
|
+
stroke="var(--color-cyan-400)"
|
|
93
93
|
unit={timeRangeBucketAverageUnit(bucketUnit)}
|
|
94
94
|
/>
|
|
95
95
|
<ActivityChartDateLabels
|
|
@@ -15,12 +15,18 @@ import { formatCostSummary } from "../../format";
|
|
|
15
15
|
|
|
16
16
|
type Widget = NonNullable<PluginOperationalReport["widgets"]>[number];
|
|
17
17
|
|
|
18
|
-
const colors = [
|
|
18
|
+
const colors = [
|
|
19
|
+
"var(--color-cyan-300)",
|
|
20
|
+
"var(--color-emerald-300)",
|
|
21
|
+
"var(--color-amber-400)",
|
|
22
|
+
"var(--color-rose-400)",
|
|
23
|
+
"var(--color-violet-400)",
|
|
24
|
+
] as const;
|
|
19
25
|
const toneColors = {
|
|
20
|
-
danger: "
|
|
21
|
-
good: "
|
|
22
|
-
neutral: "
|
|
23
|
-
warning: "
|
|
26
|
+
danger: "var(--color-rose-400)",
|
|
27
|
+
good: "var(--color-emerald-300)",
|
|
28
|
+
neutral: "var(--color-cyan-300)",
|
|
29
|
+
warning: "var(--color-amber-400)",
|
|
24
30
|
} as const;
|
|
25
31
|
|
|
26
32
|
function seriesColor(series: Widget["series"][number], index: number): string {
|
|
@@ -97,7 +103,7 @@ export function PluginBarChart(props: {
|
|
|
97
103
|
return (
|
|
98
104
|
<g key={index}>
|
|
99
105
|
<line
|
|
100
|
-
stroke="
|
|
106
|
+
stroke="var(--color-dashboard-border-subtle)"
|
|
101
107
|
strokeDasharray="3 5"
|
|
102
108
|
x1={layout.left}
|
|
103
109
|
x2={layout.width - layout.right}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { Cpu } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
import { Detail, DetailList } from "../../components/DetailList";
|
|
4
|
+
import { Card } from "../../components/layout/Card";
|
|
5
|
+
|
|
6
|
+
/** Vercel Sandbox gives each vCPU 2 GB of memory. */
|
|
7
|
+
const MEMORY_GB_PER_VCPU = 2;
|
|
8
|
+
|
|
9
|
+
/** Format a Sandbox vCPU count with its memory. Null means the Vercel default. */
|
|
10
|
+
export function formatSandboxSize(vcpus: number | null): string {
|
|
11
|
+
if (vcpus === null) return "Vercel default";
|
|
12
|
+
return `${vcpus} vCPU${vcpus === 1 ? "" : "s"} · ${vcpus * MEMORY_GB_PER_VCPU} GB`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Show one Sandbox size in the shared System summary surface. */
|
|
16
|
+
export function SandboxSizeSummary(props: {
|
|
17
|
+
description: string;
|
|
18
|
+
headingId: string;
|
|
19
|
+
title: string;
|
|
20
|
+
value: string;
|
|
21
|
+
}) {
|
|
22
|
+
return (
|
|
23
|
+
<Card as="section" className="mb-0" padding="md" variant="section">
|
|
24
|
+
<div aria-labelledby={props.headingId} className="grid gap-4">
|
|
25
|
+
<div className="min-w-0 grid gap-1">
|
|
26
|
+
<h2
|
|
27
|
+
className="m-0 text-base font-semibold text-dashboard-text"
|
|
28
|
+
id={props.headingId}
|
|
29
|
+
>
|
|
30
|
+
{props.title}
|
|
31
|
+
</h2>
|
|
32
|
+
<p className="m-0 text-sm leading-relaxed text-dashboard-text-muted">
|
|
33
|
+
{props.description}
|
|
34
|
+
</p>
|
|
35
|
+
</div>
|
|
36
|
+
<DetailList>
|
|
37
|
+
<Detail label="Size">
|
|
38
|
+
<span className="inline-flex items-center gap-2">
|
|
39
|
+
<Cpu aria-hidden="true" size={14} />
|
|
40
|
+
{props.value}
|
|
41
|
+
</span>
|
|
42
|
+
</Detail>
|
|
43
|
+
</DetailList>
|
|
44
|
+
</div>
|
|
45
|
+
</Card>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -6,16 +6,19 @@ import { WorkspaceUsageChart } from "../../components/charts/WorkspaceUsageChart
|
|
|
6
6
|
import { workspaceUsageDays } from "../../components/charts/workspaceUsage";
|
|
7
7
|
import type { TimeRangeDays } from "../../components/controls/TimeRangeSelector";
|
|
8
8
|
import { Card } from "../../components/layout/Card";
|
|
9
|
+
import { formatSandboxSize, SandboxSizeSummary } from "./SandboxSizeSummary";
|
|
9
10
|
import { SnapshotSummary } from "./SnapshotSummary";
|
|
10
11
|
|
|
11
12
|
/** Load Workspace usage stats and render snapshot plus switch volume. */
|
|
12
13
|
export function WorkspaceDetails(props: {
|
|
14
|
+
defaultVcpus: number | null;
|
|
13
15
|
range: TimeRangeDays;
|
|
14
16
|
workspace: WorkspaceReport;
|
|
15
17
|
}) {
|
|
16
18
|
const statsQuery = useStatsData();
|
|
17
19
|
return (
|
|
18
20
|
<WorkspaceDetailsContent
|
|
21
|
+
defaultVcpus={props.defaultVcpus}
|
|
19
22
|
error={Boolean(statsQuery.error)}
|
|
20
23
|
loading={statsQuery.isPending}
|
|
21
24
|
range={props.range}
|
|
@@ -25,8 +28,9 @@ export function WorkspaceDetails(props: {
|
|
|
25
28
|
);
|
|
26
29
|
}
|
|
27
30
|
|
|
28
|
-
/** Present snapshot metadata and switch volume for one Workspace recipe. */
|
|
31
|
+
/** Present snapshot metadata, Sandbox size, and switch volume for one Workspace recipe. */
|
|
29
32
|
export function WorkspaceDetailsContent(props: {
|
|
33
|
+
defaultVcpus: number | null;
|
|
30
34
|
error: boolean;
|
|
31
35
|
loading: boolean;
|
|
32
36
|
range: TimeRangeDays;
|
|
@@ -42,6 +46,12 @@ export function WorkspaceDetailsContent(props: {
|
|
|
42
46
|
snapshot={props.workspace.snapshot}
|
|
43
47
|
title="Current snapshot"
|
|
44
48
|
/>
|
|
49
|
+
<SandboxSizeSummary
|
|
50
|
+
description="Sandboxes for this Workspace start with this size."
|
|
51
|
+
headingId="workspace-sandbox-size-heading"
|
|
52
|
+
title="Sandbox size"
|
|
53
|
+
value={formatSandboxSize(props.workspace.vcpus ?? props.defaultVcpus)}
|
|
54
|
+
/>
|
|
45
55
|
{!props.stats ? (
|
|
46
56
|
<Card padding="sm">
|
|
47
57
|
<EmptyTelemetry>
|
|
@@ -6,6 +6,7 @@ import { Field } from "../../components/Field";
|
|
|
6
6
|
import { InlineError } from "../../components/InlineError";
|
|
7
7
|
import { TextArea, TextInput } from "../../components/TextInput";
|
|
8
8
|
import { Card } from "../../components/layout/Card";
|
|
9
|
+
import { formatSandboxSize } from "./SandboxSizeSummary";
|
|
9
10
|
import {
|
|
10
11
|
createRepoDraft,
|
|
11
12
|
type RepoDraft,
|
|
@@ -15,6 +16,7 @@ import {
|
|
|
15
16
|
type WorkspaceEditorProps = {
|
|
16
17
|
busy: boolean;
|
|
17
18
|
canSave: boolean;
|
|
19
|
+
defaultVcpus: number | null;
|
|
18
20
|
draft: WorkspaceDraft;
|
|
19
21
|
error?: string;
|
|
20
22
|
editing: boolean;
|
|
@@ -200,6 +202,23 @@ export function WorkspaceEditor(props: WorkspaceEditorProps) {
|
|
|
200
202
|
/>
|
|
201
203
|
</Field>
|
|
202
204
|
|
|
205
|
+
<Field
|
|
206
|
+
help="Sandbox vCPUs for this Workspace. Each vCPU adds 2 GB of memory. Leave empty to use the app default."
|
|
207
|
+
htmlFor="workspace-vcpus"
|
|
208
|
+
label="vCPUs"
|
|
209
|
+
>
|
|
210
|
+
<TextInput
|
|
211
|
+
autoComplete="off"
|
|
212
|
+
id="workspace-vcpus"
|
|
213
|
+
inputMode="numeric"
|
|
214
|
+
onChange={(event) =>
|
|
215
|
+
props.onChange({ ...props.draft, vcpus: event.target.value })
|
|
216
|
+
}
|
|
217
|
+
placeholder={formatSandboxSize(props.defaultVcpus)}
|
|
218
|
+
value={props.draft.vcpus}
|
|
219
|
+
/>
|
|
220
|
+
</Field>
|
|
221
|
+
|
|
203
222
|
{props.error ? <InlineError>{props.error}</InlineError> : null}
|
|
204
223
|
|
|
205
224
|
<div className="flex flex-wrap items-center gap-3 border-t border-dashboard-border-subtle pt-5">
|