ai-design-system 0.1.69 → 0.1.71
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/components/blocks/EvalSessionDetailsPanel/EvalSessionDetailsPanel.tsx +29 -4
- package/components/blocks/WorkflowCanvas/WorkflowCanvas.tsx +2 -2
- package/components/blocks/WorkflowCanvas/layout-engine.ts +23 -5
- package/components/composites/AppHeader/AppHeader.stories.tsx +30 -0
- package/components/composites/AppHeader/AppHeader.tsx +4 -4
- package/components/composites/AppHeader/interfaces.ts +2 -2
- package/components/composites/{WorkflowSwitcher/WorkflowSwitcher.stories.tsx → ButtonSwitcher/ButtonSwitcher.stories.tsx} +9 -11
- package/components/composites/{WorkflowSwitcher/WorkflowSwitcher.tsx → ButtonSwitcher/ButtonSwitcher.tsx} +16 -15
- package/components/composites/ButtonSwitcher/index.ts +1 -0
- package/components/composites/DashboardChart/DashboardChart.tsx +149 -31
- package/components/composites/DefaultSwitcher/DefaultSwitcher.stories.tsx +46 -0
- package/components/composites/{ThemeSelector/ThemeSelector.tsx → DefaultSwitcher/DefaultSwitcher.tsx} +8 -8
- package/components/composites/DefaultSwitcher/index.ts +2 -0
- package/components/composites/IconButton/IconButton.stories.tsx +32 -0
- package/components/composites/IconButton/IconButton.tsx +20 -0
- package/components/composites/IconButton/index.ts +2 -0
- package/components/composites/index.ts +10 -6
- package/components/features/EvalDashboardFeature/EvalDashboardFeature.mocks.ts +1 -0
- package/components/features/EvalDashboardFeature/EvalDashboardFeature.tsx +38 -23
- package/components/features/EvalDashboardFeature/useEvalDashboardFeature.d.ts +16 -0
- package/components/features/NodeEditor/NodeEditor.tsx +25 -1
- package/components/index.ts +2 -2
- package/dist/index.cjs +226 -74
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +3 -0
- package/dist/index.d.ts +96 -65
- package/dist/index.js +227 -75
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/components/composites/ThemeSelector/ThemeSelector.stories.tsx +0 -48
- package/components/composites/ThemeSelector/index.ts +0 -2
- package/components/composites/WorkflowSwitcher/index.ts +0 -1
|
@@ -11,6 +11,7 @@ export interface EvalSessionDetails {
|
|
|
11
11
|
id: string
|
|
12
12
|
date: string
|
|
13
13
|
goldenEvals: DashboardRow[]
|
|
14
|
+
recommendations: Array<{ id: number; name: string; rationale: string }>
|
|
14
15
|
systemPrompt?: string
|
|
15
16
|
outputTranscript?: string
|
|
16
17
|
}
|
|
@@ -23,12 +24,23 @@ export interface EvalSessionDetailsPanelProps {
|
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export const EvalTriggerButton = ({ onClick, loading }: { onClick: () => void; loading?: boolean }) => (
|
|
26
|
-
<Button onClick={onClick} variant="default" size="sm" className="h-8" disabled={loading}>
|
|
27
|
-
<Icon name={loading ? 'loader-2' : 'play'} className={`w-4 h-4
|
|
28
|
-
{loading ? 'Evaluating...' : 'Trigger'}
|
|
27
|
+
<Button onClick={onClick} variant="default" size="sm" className="h-8 w-8 p-0" disabled={loading} title={loading ? 'Evaluating...' : 'Trigger'}>
|
|
28
|
+
<Icon name={loading ? 'loader-2' : 'play'} className={`w-4 h-4${loading ? ' animate-spin' : ''}`} />
|
|
29
29
|
</Button>
|
|
30
30
|
)
|
|
31
31
|
|
|
32
|
+
const recommendationTableSchema = dynamicTableSchema.parse({
|
|
33
|
+
schemaVersion: DYNAMIC_TABLE_SCHEMA_VERSION,
|
|
34
|
+
rowKey: "name",
|
|
35
|
+
enableFiltering: true,
|
|
36
|
+
enablePagination: false,
|
|
37
|
+
enableRowSelection: false,
|
|
38
|
+
columns: [
|
|
39
|
+
{ key: "name", label: "Recommendation", renderType: "text" },
|
|
40
|
+
{ key: "rationale", label: "Rationale", renderType: "text" },
|
|
41
|
+
],
|
|
42
|
+
})
|
|
43
|
+
|
|
32
44
|
const goldenEvalTableSchema = dynamicTableSchema.parse({
|
|
33
45
|
schemaVersion: DYNAMIC_TABLE_SCHEMA_VERSION,
|
|
34
46
|
rowKey: "id",
|
|
@@ -44,7 +56,7 @@ const goldenEvalTableSchema = dynamicTableSchema.parse({
|
|
|
44
56
|
})
|
|
45
57
|
|
|
46
58
|
export const EvalSessionDetailsPanel = React.memo<EvalSessionDetailsPanelProps>(
|
|
47
|
-
({ sessionDetails, activeTab, actionHandlers, workflowContent }) => {
|
|
59
|
+
({ sessionDetails, activeTab, actionHandlers: _actionHandlers, workflowContent }) => {
|
|
48
60
|
return (
|
|
49
61
|
<div className="flex flex-col h-full bg-background">
|
|
50
62
|
<Tabs value={activeTab} className="flex-1 flex flex-col min-h-0">
|
|
@@ -62,6 +74,19 @@ export const EvalSessionDetailsPanel = React.memo<EvalSessionDetailsPanelProps>(
|
|
|
62
74
|
</div>
|
|
63
75
|
</TabsContent>
|
|
64
76
|
|
|
77
|
+
<TabsContent value="recommendations" className="absolute inset-0 m-0 p-0 overflow-hidden flex flex-col">
|
|
78
|
+
<div className="p-4 flex-1 min-h-0 flex flex-col">
|
|
79
|
+
{sessionDetails.recommendations && sessionDetails.recommendations.length > 0 ? (
|
|
80
|
+
<EnhancedDataTable
|
|
81
|
+
data={sessionDetails.recommendations}
|
|
82
|
+
tableSchema={recommendationTableSchema}
|
|
83
|
+
/>
|
|
84
|
+
) : (
|
|
85
|
+
<div className="text-sm text-muted-foreground flex-1 flex items-center justify-center">No recommendations available.</div>
|
|
86
|
+
)}
|
|
87
|
+
</div>
|
|
88
|
+
</TabsContent>
|
|
89
|
+
|
|
65
90
|
<TabsContent value="prompts" className="absolute inset-0 m-0 p-0">
|
|
66
91
|
<ScrollArea className="h-full">
|
|
67
92
|
<div className="p-4">
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
ConnectionMode,
|
|
5
5
|
ReactFlowProvider,
|
|
6
6
|
type Connection,
|
|
7
|
+
type Node,
|
|
7
8
|
useReactFlow,
|
|
8
9
|
} from "@xyflow/react";
|
|
9
10
|
import { useCallback, useEffect } from "react";
|
|
@@ -95,8 +96,7 @@ function WorkflowCanvasInner({
|
|
|
95
96
|
onEdgesChange={onEdgesChange}
|
|
96
97
|
onNodesChange={onNodesChange}
|
|
97
98
|
onPaneClick={onPaneClick}
|
|
98
|
-
|
|
99
|
-
onNodeClick={onNodeClick as any}
|
|
99
|
+
onNodeClick={onNodeClick as (event: React.MouseEvent, node: Node) => void}
|
|
100
100
|
onEdgeClick={onEdgeClick}
|
|
101
101
|
>
|
|
102
102
|
{topLeft && (
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
import ELK from 'elkjs';
|
|
2
2
|
import type { WorkflowNode, WorkflowEdge } from './interfaces';
|
|
3
3
|
|
|
4
|
+
interface ElkNode {
|
|
5
|
+
id: string;
|
|
6
|
+
x?: number;
|
|
7
|
+
y?: number;
|
|
8
|
+
width?: number;
|
|
9
|
+
height?: number;
|
|
10
|
+
children?: ElkNode[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ElkGraph {
|
|
14
|
+
id: string;
|
|
15
|
+
layoutOptions?: Record<string, string>;
|
|
16
|
+
children?: ElkNode[];
|
|
17
|
+
edges?: Array<{
|
|
18
|
+
id: string;
|
|
19
|
+
sources: string[];
|
|
20
|
+
targets: string[];
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
|
|
4
24
|
const NODE_WIDTH = 180;
|
|
5
25
|
const NODE_HEIGHT = 52;
|
|
6
26
|
|
|
@@ -10,7 +30,7 @@ export async function getLayoutedElements(
|
|
|
10
30
|
nodes: WorkflowNode[],
|
|
11
31
|
edges: WorkflowEdge[],
|
|
12
32
|
): Promise<{ nodes: WorkflowNode[]; edges: WorkflowEdge[] }> {
|
|
13
|
-
const graph = {
|
|
33
|
+
const graph: ElkGraph = {
|
|
14
34
|
id: 'root',
|
|
15
35
|
layoutOptions: {
|
|
16
36
|
'elk.algorithm': 'layered',
|
|
@@ -36,12 +56,10 @@ export async function getLayoutedElements(
|
|
|
36
56
|
}),
|
|
37
57
|
};
|
|
38
58
|
|
|
39
|
-
|
|
40
|
-
const layout = await elk.layout(graph as any);
|
|
59
|
+
const layout = await elk.layout(graph);
|
|
41
60
|
|
|
42
61
|
const layoutedNodes: WorkflowNode[] = nodes.map((n) => {
|
|
43
|
-
|
|
44
|
-
const elkNode = (layout.children ?? []).find((c: any) => c.id === n.id);
|
|
62
|
+
const elkNode = (layout.children ?? []).find((c: ElkNode) => c.id === n.id);
|
|
45
63
|
if (!elkNode || elkNode.x == null || elkNode.y == null) return n;
|
|
46
64
|
return {
|
|
47
65
|
...n,
|
|
@@ -144,3 +144,33 @@ export const ManyTabs: Story = {
|
|
|
144
144
|
</SidebarProvider>
|
|
145
145
|
),
|
|
146
146
|
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* With Button Switcher
|
|
150
|
+
*
|
|
151
|
+
* Header with a button switcher (dropdown) instead of a title.
|
|
152
|
+
*/
|
|
153
|
+
export const WithButtonSwitcher: Story = {
|
|
154
|
+
render: () => (
|
|
155
|
+
<SidebarProvider>
|
|
156
|
+
<Sidebar />
|
|
157
|
+
<SidebarInset>
|
|
158
|
+
<AppHeader
|
|
159
|
+
buttonSwitcherProps={{
|
|
160
|
+
items: [
|
|
161
|
+
{ id: "1", name: "Alpha Workflow" },
|
|
162
|
+
{ id: "2", name: "Beta Workflow" },
|
|
163
|
+
],
|
|
164
|
+
activeId: "1",
|
|
165
|
+
onSelect: fn(),
|
|
166
|
+
}}
|
|
167
|
+
actions={
|
|
168
|
+
<Button size="sm">
|
|
169
|
+
Deploy
|
|
170
|
+
</Button>
|
|
171
|
+
}
|
|
172
|
+
/>
|
|
173
|
+
</SidebarInset>
|
|
174
|
+
</SidebarProvider>
|
|
175
|
+
),
|
|
176
|
+
}
|
|
@@ -3,7 +3,7 @@ import { SidebarTrigger } from "@/components/primitives/Sidebar"
|
|
|
3
3
|
import { Separator } from "@/components/primitives/Separator"
|
|
4
4
|
import { Tabs, TabsList, TabsTrigger } from "@/components/primitives/Tabs"
|
|
5
5
|
import { ChatToggleButton } from "@/components/composites/ChatToggleButton"
|
|
6
|
-
import {
|
|
6
|
+
import { ButtonSwitcher } from "@/components/composites/ButtonSwitcher"
|
|
7
7
|
import type { AppHeaderProps } from "./interfaces"
|
|
8
8
|
|
|
9
9
|
export const AppHeader = React.memo<AppHeaderProps>(({
|
|
@@ -16,7 +16,7 @@ export const AppHeader = React.memo<AppHeaderProps>(({
|
|
|
16
16
|
tabsPosition = 'center',
|
|
17
17
|
showSidebarToggle = true,
|
|
18
18
|
showTitle = true,
|
|
19
|
-
|
|
19
|
+
buttonSwitcherProps,
|
|
20
20
|
chatToggleProps
|
|
21
21
|
}) => {
|
|
22
22
|
return (
|
|
@@ -24,7 +24,7 @@ export const AppHeader = React.memo<AppHeaderProps>(({
|
|
|
24
24
|
<div className="grid w-full grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center px-4 lg:px-6">
|
|
25
25
|
<div className="min-w-0 flex items-center gap-1 lg:gap-2">
|
|
26
26
|
{showSidebarToggle && <SidebarTrigger className="-ml-1" />}
|
|
27
|
-
{showSidebarToggle && (showTitle && title ||
|
|
27
|
+
{showSidebarToggle && (showTitle && title || buttonSwitcherProps || chatToggleProps) && (
|
|
28
28
|
<Separator orientation="vertical" className="mx-2 data-[orientation=vertical]:h-4" />
|
|
29
29
|
)}
|
|
30
30
|
{showTitle && title && (
|
|
@@ -44,7 +44,7 @@ export const AppHeader = React.memo<AppHeaderProps>(({
|
|
|
44
44
|
</TabsList>
|
|
45
45
|
</Tabs>
|
|
46
46
|
)}
|
|
47
|
-
{
|
|
47
|
+
{buttonSwitcherProps && <ButtonSwitcher {...buttonSwitcherProps} />}
|
|
48
48
|
</div>
|
|
49
49
|
|
|
50
50
|
<div className="justify-self-center">
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type React from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type { ButtonSwitcherProps } from "@/components/composites/ButtonSwitcher/ButtonSwitcher";
|
|
3
3
|
import type { ChatToggleButtonProps } from "@/components/composites/ChatToggleButton/ChatToggleButton";
|
|
4
4
|
|
|
5
5
|
export interface TabItem {
|
|
@@ -17,6 +17,6 @@ export interface AppHeaderProps {
|
|
|
17
17
|
className?: string;
|
|
18
18
|
showSidebarToggle?: boolean;
|
|
19
19
|
showTitle?: boolean;
|
|
20
|
-
|
|
20
|
+
buttonSwitcherProps?: ButtonSwitcherProps;
|
|
21
21
|
chatToggleProps?: ChatToggleButtonProps;
|
|
22
22
|
}
|
|
@@ -1,26 +1,24 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
2
|
import type { Meta, StoryObj } from "@storybook/react"
|
|
3
|
-
import {
|
|
3
|
+
import { ButtonSwitcher } from "./ButtonSwitcher"
|
|
4
4
|
|
|
5
|
-
const meta: Meta<typeof
|
|
6
|
-
title: "Composites/
|
|
7
|
-
component:
|
|
8
|
-
|
|
9
|
-
layout: "centered",
|
|
10
|
-
},
|
|
5
|
+
const meta: Meta<typeof ButtonSwitcher> = {
|
|
6
|
+
title: "Composites/ButtonSwitcher",
|
|
7
|
+
component: ButtonSwitcher,
|
|
8
|
+
tags: ["autodocs"],
|
|
11
9
|
args: {
|
|
12
|
-
|
|
10
|
+
items: [
|
|
13
11
|
{ id: "1", name: "Alpha Workflow" },
|
|
14
12
|
{ id: "2", name: "Beta Workflow" },
|
|
15
13
|
{ id: "3", name: "Gamma Workflow" },
|
|
16
14
|
],
|
|
17
|
-
|
|
18
|
-
|
|
15
|
+
activeId: "1",
|
|
16
|
+
onSelect: () => {},
|
|
19
17
|
},
|
|
20
18
|
}
|
|
21
19
|
|
|
22
20
|
export default meta
|
|
23
|
-
type Story = StoryObj<typeof
|
|
21
|
+
type Story = StoryObj<typeof ButtonSwitcher>
|
|
24
22
|
|
|
25
23
|
export const Default: Story = {}
|
|
26
24
|
|
|
@@ -8,21 +8,22 @@ import {
|
|
|
8
8
|
} from "@/components/primitives/DropdownMenu"
|
|
9
9
|
import { Icon } from "@/components/primitives/Icon"
|
|
10
10
|
|
|
11
|
-
export interface
|
|
11
|
+
export interface ButtonSwitcherItem {
|
|
12
12
|
id: string
|
|
13
13
|
name: string
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
export interface
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
export interface ButtonSwitcherProps {
|
|
17
|
+
items: ButtonSwitcherItem[]
|
|
18
|
+
activeId?: string | null
|
|
19
|
+
onSelect: (id: string) => void
|
|
20
20
|
className?: string
|
|
21
|
+
placeholder?: string
|
|
21
22
|
}
|
|
22
23
|
|
|
23
|
-
export const
|
|
24
|
-
({
|
|
25
|
-
const
|
|
24
|
+
export const ButtonSwitcher = React.memo<ButtonSwitcherProps>(
|
|
25
|
+
({ items, activeId, onSelect, className, placeholder = "Select Item" }) => {
|
|
26
|
+
const currentItem = items.find((w) => w.id === activeId)
|
|
26
27
|
|
|
27
28
|
return (
|
|
28
29
|
<div className={className}>
|
|
@@ -35,25 +36,25 @@ export const WorkflowSwitcher = React.memo<WorkflowSwitcherProps>(
|
|
|
35
36
|
variant="secondary"
|
|
36
37
|
>
|
|
37
38
|
<span className="truncate max-w-[150px]">
|
|
38
|
-
{
|
|
39
|
+
{currentItem?.name ?? placeholder}
|
|
39
40
|
</span>
|
|
40
41
|
<Icon name="chevron-down" size="xs" className="ml-1 opacity-50 shrink-0" />
|
|
41
42
|
</Button>
|
|
42
43
|
</DropdownMenuTrigger>
|
|
43
44
|
<DropdownMenuContent align="start">
|
|
44
|
-
{
|
|
45
|
+
{items.map((w) => (
|
|
45
46
|
<DropdownMenuItem
|
|
46
47
|
className="flex items-center justify-between"
|
|
47
48
|
key={w.id}
|
|
48
|
-
onClick={() =>
|
|
49
|
+
onClick={() => onSelect(w.id)}
|
|
49
50
|
>
|
|
50
51
|
<span className="truncate pr-4">{w.name}</span>
|
|
51
|
-
{w.id ===
|
|
52
|
+
{w.id === activeId && <Icon name="check" size="sm" className="ml-auto shrink-0" />}
|
|
52
53
|
</DropdownMenuItem>
|
|
53
54
|
))}
|
|
54
|
-
{
|
|
55
|
+
{items.length === 0 && (
|
|
55
56
|
<div className="px-2 py-1.5 text-sm text-muted-foreground text-center">
|
|
56
|
-
No
|
|
57
|
+
No items found
|
|
57
58
|
</div>
|
|
58
59
|
)}
|
|
59
60
|
</DropdownMenuContent>
|
|
@@ -63,4 +64,4 @@ export const WorkflowSwitcher = React.memo<WorkflowSwitcherProps>(
|
|
|
63
64
|
}
|
|
64
65
|
)
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
ButtonSwitcher.displayName = "ButtonSwitcher"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./ButtonSwitcher"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as React from "react"
|
|
2
|
-
import { Area,
|
|
2
|
+
import { Area, CartesianGrid, ComposedChart, Line, XAxis } from "recharts"
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
ChartContainer,
|
|
@@ -30,10 +30,11 @@ export interface DashboardChartPoint {
|
|
|
30
30
|
mobile: number
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
export type DashboardChartTimeRange = string
|
|
33
|
+
export type DashboardChartTimeRange = string;
|
|
34
34
|
|
|
35
35
|
export interface DashboardChartProps {
|
|
36
36
|
series: DashboardChartPoint[]
|
|
37
|
+
timeRange?: DashboardChartTimeRange
|
|
37
38
|
onTimeRangeChange?: (range: DashboardChartTimeRange) => void
|
|
38
39
|
title: string
|
|
39
40
|
description: string
|
|
@@ -46,8 +47,100 @@ export interface DashboardChartProps {
|
|
|
46
47
|
chartClassName?: string
|
|
47
48
|
}
|
|
48
49
|
|
|
50
|
+
interface DashboardTooltipPayloadItem {
|
|
51
|
+
dataKey?: string | number;
|
|
52
|
+
value?: number;
|
|
53
|
+
[key: string]: unknown;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const DashboardTooltipContent = React.memo((props: {
|
|
57
|
+
active?: boolean;
|
|
58
|
+
payload?: DashboardTooltipPayloadItem[];
|
|
59
|
+
label?: string;
|
|
60
|
+
desktopLabel?: string;
|
|
61
|
+
mobileLabel?: string;
|
|
62
|
+
[key: string]: unknown;
|
|
63
|
+
}): React.ReactElement | null => {
|
|
64
|
+
const { active, payload, desktopLabel, mobileLabel, content: _content, label, ...rest } = props;
|
|
65
|
+
void _content;
|
|
66
|
+
if (!active || !payload?.length) return null;
|
|
67
|
+
|
|
68
|
+
const uniquePayload = payload.filter(
|
|
69
|
+
(item: DashboardTooltipPayloadItem, index: number, self: DashboardTooltipPayloadItem[]) =>
|
|
70
|
+
index === self.findIndex((t: DashboardTooltipPayloadItem) => String(t.dataKey) === String(item.dataKey)),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<ChartTooltipContent
|
|
75
|
+
{...rest}
|
|
76
|
+
active={active}
|
|
77
|
+
label={label as string}
|
|
78
|
+
payload={uniquePayload}
|
|
79
|
+
labelFormatter={(value, tooltipPayload) => {
|
|
80
|
+
const typedPayload = tooltipPayload as Array<{ payload: { timestamp?: number } }>;
|
|
81
|
+
const timestamp = typedPayload?.[0]?.payload?.timestamp;
|
|
82
|
+
if (!timestamp) return null;
|
|
83
|
+
const date = new Date(timestamp);
|
|
84
|
+
if (isNaN(date.getTime())) return ""
|
|
85
|
+
return date.toLocaleDateString("en-US", {
|
|
86
|
+
month: "short",
|
|
87
|
+
year: "numeric",
|
|
88
|
+
})
|
|
89
|
+
}}
|
|
90
|
+
formatter={(value, name, item, index, tooltipPayload) => {
|
|
91
|
+
const typedPayload = tooltipPayload as { timestamp?: number };
|
|
92
|
+
const typedItem = item as { color?: string };
|
|
93
|
+
const labelText = name === "desktop" ? desktopLabel : (name === "mobile" ? mobileLabel : name);
|
|
94
|
+
const timeStr = typedPayload?.timestamp ? new Date(typedPayload.timestamp).toLocaleTimeString("en-US", {
|
|
95
|
+
hour: "numeric",
|
|
96
|
+
minute: "2-digit"
|
|
97
|
+
}) : null;
|
|
98
|
+
|
|
99
|
+
return (
|
|
100
|
+
<div className="flex flex-col w-full">
|
|
101
|
+
<div className="flex w-full items-center gap-2">
|
|
102
|
+
<div
|
|
103
|
+
className="h-2.5 w-2.5 shrink-0 rounded-[2px]"
|
|
104
|
+
style={{ backgroundColor: typedItem.color }}
|
|
105
|
+
/>
|
|
106
|
+
<div className="flex flex-1 justify-between items-center gap-4">
|
|
107
|
+
<span className="text-muted-foreground">{labelText as React.ReactNode}</span>
|
|
108
|
+
<span className="font-mono font-medium tabular-nums text-foreground">{value as React.ReactNode}</span>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
{timeStr && (
|
|
112
|
+
<div className="text-[10px] text-muted-foreground self-end mt-0.5">
|
|
113
|
+
{timeStr}
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
</div>
|
|
117
|
+
)
|
|
118
|
+
}}
|
|
119
|
+
indicator="dot"
|
|
120
|
+
/>
|
|
121
|
+
);
|
|
122
|
+
});
|
|
123
|
+
DashboardTooltipContent.displayName = "DashboardTooltipContent";
|
|
124
|
+
|
|
125
|
+
const CustomDot = React.memo((props: { cx?: number; cy?: number; stroke?: string }): React.ReactElement => {
|
|
126
|
+
const { cx, cy, stroke } = props;
|
|
127
|
+
return (
|
|
128
|
+
<circle
|
|
129
|
+
cx={cx}
|
|
130
|
+
cy={cy}
|
|
131
|
+
r={4}
|
|
132
|
+
fill="var(--background)"
|
|
133
|
+
stroke={stroke}
|
|
134
|
+
strokeWidth={2}
|
|
135
|
+
pointerEvents="none"
|
|
136
|
+
/>
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
CustomDot.displayName = "CustomDot";
|
|
140
|
+
|
|
49
141
|
export const DashboardChart = React.memo<DashboardChartProps>(({
|
|
50
142
|
series,
|
|
143
|
+
timeRange: propsTimeRange,
|
|
51
144
|
onTimeRangeChange,
|
|
52
145
|
title,
|
|
53
146
|
description,
|
|
@@ -59,32 +152,41 @@ export const DashboardChart = React.memo<DashboardChartProps>(({
|
|
|
59
152
|
className,
|
|
60
153
|
chartClassName
|
|
61
154
|
}) => {
|
|
62
|
-
const [
|
|
155
|
+
const [localTimeRange, setLocalTimeRange] = React.useState<DashboardChartTimeRange>(timeRanges[0]?.value)
|
|
156
|
+
const timeRange = propsTimeRange ?? localTimeRange
|
|
63
157
|
|
|
64
158
|
const handleTimeRangeChange = React.useCallback(
|
|
65
159
|
(range: DashboardChartTimeRange) => {
|
|
66
|
-
|
|
160
|
+
setLocalTimeRange(range)
|
|
67
161
|
onTimeRangeChange?.(range)
|
|
68
162
|
},
|
|
69
163
|
[onTimeRangeChange]
|
|
70
164
|
)
|
|
71
165
|
|
|
72
166
|
const filteredSeries = React.useMemo(() => {
|
|
73
|
-
|
|
167
|
+
// 1. Sort ascending chronologically (oldest to newest)
|
|
168
|
+
const sortedSeries = [...series].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
|
|
169
|
+
|
|
170
|
+
const referenceDate = sortedSeries.length > 0 ? sortedSeries[sortedSeries.length - 1].date : new Date().toISOString()
|
|
74
171
|
let daysToSubtract = 90
|
|
75
172
|
if (timeRange === "30d") {
|
|
76
173
|
daysToSubtract = 30
|
|
77
174
|
} else if (timeRange === "7d") {
|
|
78
175
|
daysToSubtract = 7
|
|
79
|
-
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let filtered = sortedSeries;
|
|
179
|
+
if (timeRange === "10" || timeRange === "20" || timeRange === "30") {
|
|
80
180
|
// Special logic for count-based limits if passed as timeRange
|
|
81
181
|
const limit = parseInt(timeRange, 10)
|
|
82
|
-
|
|
182
|
+
filtered = sortedSeries.slice(-limit)
|
|
183
|
+
} else {
|
|
184
|
+
const startDate = new Date(referenceDate)
|
|
185
|
+
startDate.setDate(startDate.getDate() - daysToSubtract)
|
|
186
|
+
filtered = sortedSeries.filter(item => new Date(item.date) >= startDate)
|
|
83
187
|
}
|
|
84
188
|
|
|
85
|
-
|
|
86
|
-
startDate.setDate(startDate.getDate() - daysToSubtract)
|
|
87
|
-
return series.filter(item => new Date(item.date) >= startDate)
|
|
189
|
+
return filtered.map(item => ({ ...item, timestamp: new Date(item.date).getTime() }))
|
|
88
190
|
}, [series, timeRange])
|
|
89
191
|
|
|
90
192
|
return (
|
|
@@ -137,7 +239,7 @@ export const DashboardChart = React.memo<DashboardChartProps>(({
|
|
|
137
239
|
}}
|
|
138
240
|
className={cn("aspect-auto w-full", chartClassName || "h-[250px]")}
|
|
139
241
|
>
|
|
140
|
-
<
|
|
242
|
+
<ComposedChart data={filteredSeries}>
|
|
141
243
|
<defs>
|
|
142
244
|
<linearGradient id="fillDesktop" x1="0" y1="0" x2="0" y2="1">
|
|
143
245
|
<stop offset="5%" stopColor="var(--color-desktop)" stopOpacity={1} />
|
|
@@ -152,50 +254,66 @@ export const DashboardChart = React.memo<DashboardChartProps>(({
|
|
|
152
254
|
</defs>
|
|
153
255
|
<CartesianGrid vertical={false} />
|
|
154
256
|
<XAxis
|
|
155
|
-
dataKey="
|
|
257
|
+
dataKey="timestamp"
|
|
258
|
+
type="number"
|
|
259
|
+
scale="time"
|
|
260
|
+
domain={['dataMin', 'dataMax']}
|
|
156
261
|
tickLine={false}
|
|
157
262
|
axisLine={false}
|
|
158
263
|
tickMargin={8}
|
|
159
264
|
minTickGap={32}
|
|
160
265
|
tickFormatter={(value) => {
|
|
161
266
|
const date = new Date(value)
|
|
162
|
-
return date.
|
|
267
|
+
return date.toLocaleString("en-US", {
|
|
163
268
|
month: "short",
|
|
164
269
|
day: "numeric",
|
|
270
|
+
hour: "numeric",
|
|
271
|
+
minute: "2-digit"
|
|
165
272
|
})
|
|
166
273
|
}}
|
|
167
274
|
/>
|
|
168
275
|
<ChartTooltip
|
|
169
276
|
cursor={false}
|
|
170
277
|
content={
|
|
171
|
-
<
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
month: "short",
|
|
175
|
-
day: "numeric",
|
|
176
|
-
})
|
|
177
|
-
}}
|
|
178
|
-
indicator="dot"
|
|
278
|
+
<DashboardTooltipContent
|
|
279
|
+
desktopLabel={desktopLabel}
|
|
280
|
+
mobileLabel={mobileLabel}
|
|
179
281
|
/>
|
|
180
282
|
}
|
|
181
283
|
/>
|
|
182
284
|
{showMobile && (
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
285
|
+
<>
|
|
286
|
+
<Area
|
|
287
|
+
dataKey="mobile"
|
|
288
|
+
type="monotone"
|
|
289
|
+
fill="url(#fillMobile)"
|
|
290
|
+
stroke="none"
|
|
291
|
+
/>
|
|
292
|
+
<Line
|
|
293
|
+
dataKey="mobile"
|
|
294
|
+
type="monotone"
|
|
295
|
+
stroke="var(--color-mobile)"
|
|
296
|
+
strokeWidth={2}
|
|
297
|
+
dot={<CustomDot />}
|
|
298
|
+
activeDot={{ r: 6 }}
|
|
299
|
+
/>
|
|
300
|
+
</>
|
|
190
301
|
)}
|
|
191
302
|
<Area
|
|
192
303
|
dataKey="desktop"
|
|
193
|
-
type="
|
|
304
|
+
type="monotone"
|
|
194
305
|
fill="url(#fillDesktop)"
|
|
306
|
+
stroke="none"
|
|
307
|
+
/>
|
|
308
|
+
<Line
|
|
309
|
+
dataKey="desktop"
|
|
310
|
+
type="monotone"
|
|
195
311
|
stroke="var(--color-desktop)"
|
|
196
|
-
|
|
312
|
+
strokeWidth={2}
|
|
313
|
+
dot={<CustomDot />}
|
|
314
|
+
activeDot={{ r: 6 }}
|
|
197
315
|
/>
|
|
198
|
-
</
|
|
316
|
+
</ComposedChart>
|
|
199
317
|
</ChartContainer>
|
|
200
318
|
</CardContent>
|
|
201
319
|
</Card>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
import { DefaultSwitcher, DefaultSwitcherItem } from "./DefaultSwitcher"
|
|
3
|
+
import type { Meta, StoryObj } from "@storybook/react"
|
|
4
|
+
|
|
5
|
+
const meta = {
|
|
6
|
+
title: "Composites/DefaultSwitcher",
|
|
7
|
+
component: DefaultSwitcher,
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: "centered",
|
|
10
|
+
},
|
|
11
|
+
tags: ["autodocs"],
|
|
12
|
+
} satisfies Meta<typeof DefaultSwitcher>
|
|
13
|
+
|
|
14
|
+
export default meta
|
|
15
|
+
type Story = StoryObj<typeof meta>
|
|
16
|
+
|
|
17
|
+
export const Default: Story = {
|
|
18
|
+
args: {
|
|
19
|
+
themes: [
|
|
20
|
+
{ label: "Light", value: "light" },
|
|
21
|
+
{ label: "Dark", value: "dark" },
|
|
22
|
+
{ label: "System", value: "system" },
|
|
23
|
+
],
|
|
24
|
+
placeholder: "Select theme...",
|
|
25
|
+
},
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const Controlled: Story = {
|
|
29
|
+
render: () => {
|
|
30
|
+
const [value, setValue] = React.useState<string>()
|
|
31
|
+
const themes: DefaultSwitcherItem[] = [
|
|
32
|
+
{ label: "Blue", value: "blue" },
|
|
33
|
+
{ label: "Green", value: "green" },
|
|
34
|
+
{ label: "Purple", value: "purple" },
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="flex flex-col gap-4 min-w-[200px]">
|
|
39
|
+
<div className="text-sm text-muted-foreground">Selected: {value || "None"}</div>
|
|
40
|
+
<DefaultSwitcher themes={themes} value={value} onValueChange={setValue} />
|
|
41
|
+
|
|
42
|
+
<p style={{ marginTop: '16px', fontSize: '14px' }}>Selected: {value}</p>
|
|
43
|
+
</div>
|
|
44
|
+
)
|
|
45
|
+
},
|
|
46
|
+
}
|