ai-design-system 0.1.25 → 0.1.27
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/InboxPanel/InboxPanel.stories.tsx +48 -0
- package/components/blocks/InboxPanel/InboxPanel.tsx +55 -0
- package/components/blocks/InboxPanel/index.ts +2 -0
- package/components/blocks/SectionLayout/SectionLayout.tsx +2 -0
- package/components/blocks/SectionLayout/interfaces.ts +2 -0
- package/components/blocks/index.ts +3 -0
- package/components/composites/AdjustableLayout/AdjustableLayout.tsx +15 -2
- package/components/composites/DataTable/EnhancedDataTable.behaviors.stories.tsx +20 -3
- package/components/composites/DataTable/EnhancedDataTable.stories.tsx +18 -0
- package/components/composites/DataTable/EnhancedDataTable.tsx +2 -2
- package/components/composites/InboxList/InboxList.stories.tsx +45 -0
- package/components/composites/InboxList/InboxList.tsx +99 -0
- package/components/composites/InboxList/index.ts +2 -0
- package/components/composites/index.ts +4 -0
- package/components/features/WorkflowObservabilityFeature/WorkflowObservabilityFeature.behaviors.stories.tsx +34 -5
- package/components/features/WorkflowObservabilityFeature/WorkflowObservabilityFeature.mocks.ts +20 -0
- package/components/features/WorkflowObservabilityFeature/WorkflowObservabilityFeature.stories.tsx +31 -11
- package/components/features/WorkflowObservabilityFeature/WorkflowObservabilityFeature.tsx +68 -8
- package/components/features/WorkflowObservabilityFeature/useWorkflowObservabilityFeature.d.ts +12 -0
- package/components/features/WorkflowObservabilityFeature/useWorkflowObservabilityFeature.mock.ts +48 -0
- package/components/features/index.ts +1 -0
- package/dist/index.cjs +184 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +46 -6
- package/dist/index.d.ts +26 -0
- package/dist/index.js +183 -12
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/nextjs-vite"
|
|
2
|
+
|
|
3
|
+
import { InboxPanel } from "./InboxPanel"
|
|
4
|
+
|
|
5
|
+
const items = Array.from({ length: 30 }).map((_, index) => ({
|
|
6
|
+
id: `item-${index + 1}`,
|
|
7
|
+
title: `Item ${index + 1}`,
|
|
8
|
+
subtitle: "completed • 12s • retries 0",
|
|
9
|
+
preview: "Inbox-style preview text for validating panel-level vertical scrolling behavior.",
|
|
10
|
+
timestamp: `${index + 1}m ago`,
|
|
11
|
+
badge: index % 3 === 0 ? "running" : "completed",
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
const meta = {
|
|
15
|
+
title: "Blocks/InboxPanel",
|
|
16
|
+
component: InboxPanel,
|
|
17
|
+
tags: ["autodocs"],
|
|
18
|
+
parameters: {
|
|
19
|
+
layout: "fullscreen",
|
|
20
|
+
},
|
|
21
|
+
} satisfies Meta<typeof InboxPanel>
|
|
22
|
+
|
|
23
|
+
export default meta
|
|
24
|
+
type Story = StoryObj<typeof meta>
|
|
25
|
+
|
|
26
|
+
export const Default: Story = {
|
|
27
|
+
args: {
|
|
28
|
+
items,
|
|
29
|
+
selectedItemId: "item-2",
|
|
30
|
+
},
|
|
31
|
+
render: (args) => (
|
|
32
|
+
<div className="h-[720px]">
|
|
33
|
+
<InboxPanel {...args} />
|
|
34
|
+
</div>
|
|
35
|
+
),
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const Empty: Story = {
|
|
39
|
+
args: {
|
|
40
|
+
items: [],
|
|
41
|
+
emptyMessage: "No matching items.",
|
|
42
|
+
},
|
|
43
|
+
render: (args) => (
|
|
44
|
+
<div className="h-[720px]">
|
|
45
|
+
<InboxPanel {...args} />
|
|
46
|
+
</div>
|
|
47
|
+
),
|
|
48
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { InboxList, type InboxListItem } from "@/components/composites/InboxList"
|
|
4
|
+
import { Input } from "@/components/primitives/Input"
|
|
5
|
+
|
|
6
|
+
export interface InboxPanelProps {
|
|
7
|
+
items: InboxListItem[]
|
|
8
|
+
selectedItemId?: string | null
|
|
9
|
+
onSelectItem?: (itemId: string) => void
|
|
10
|
+
searchQuery?: string
|
|
11
|
+
onSearchQueryChange?: (value: string) => void
|
|
12
|
+
searchPlaceholder?: string
|
|
13
|
+
isLoading?: boolean
|
|
14
|
+
emptyMessage?: string
|
|
15
|
+
className?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const InboxPanel = React.memo<InboxPanelProps>(
|
|
19
|
+
({
|
|
20
|
+
items,
|
|
21
|
+
selectedItemId,
|
|
22
|
+
onSelectItem,
|
|
23
|
+
searchQuery = "",
|
|
24
|
+
onSearchQueryChange,
|
|
25
|
+
searchPlaceholder = "Type to search...",
|
|
26
|
+
isLoading = false,
|
|
27
|
+
emptyMessage,
|
|
28
|
+
className,
|
|
29
|
+
}) => {
|
|
30
|
+
return (
|
|
31
|
+
<section className={`flex min-h-0 flex-1 flex-col overflow-hidden ${className ?? ""}`}>
|
|
32
|
+
<div className="p-4">
|
|
33
|
+
<Input
|
|
34
|
+
aria-label="Search inbox items"
|
|
35
|
+
className="h-8"
|
|
36
|
+
onChange={(event) => onSearchQueryChange?.(event.target.value)}
|
|
37
|
+
placeholder={searchPlaceholder}
|
|
38
|
+
value={searchQuery}
|
|
39
|
+
/>
|
|
40
|
+
</div>
|
|
41
|
+
|
|
42
|
+
<InboxList
|
|
43
|
+
className="min-h-0 flex-1"
|
|
44
|
+
emptyMessage={emptyMessage}
|
|
45
|
+
isLoading={isLoading}
|
|
46
|
+
items={items}
|
|
47
|
+
onSelectItem={onSelectItem}
|
|
48
|
+
selectedItemId={selectedItemId}
|
|
49
|
+
/>
|
|
50
|
+
</section>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
InboxPanel.displayName = "InboxPanel"
|
|
@@ -18,6 +18,7 @@ export const SectionLayout = React.memo<SectionLayoutProps>(
|
|
|
18
18
|
orientation = "horizontal",
|
|
19
19
|
storageKey,
|
|
20
20
|
onSectionResize,
|
|
21
|
+
resizable = true,
|
|
21
22
|
dragHandleColor = "border",
|
|
22
23
|
className,
|
|
23
24
|
...props
|
|
@@ -25,6 +26,7 @@ export const SectionLayout = React.memo<SectionLayoutProps>(
|
|
|
25
26
|
// Transform sections to include headers
|
|
26
27
|
const transformedSections = sections.map(section => ({
|
|
27
28
|
...section,
|
|
29
|
+
resizable,
|
|
28
30
|
content: (
|
|
29
31
|
<div className="h-full min-h-0 flex flex-col overflow-hidden">
|
|
30
32
|
{section.header && (
|
|
@@ -4,6 +4,7 @@ import type { AppHeaderProps } from "@/components/composites/AppHeader/interface
|
|
|
4
4
|
export interface SectionLayoutSection {
|
|
5
5
|
id: string;
|
|
6
6
|
content: React.ReactNode;
|
|
7
|
+
fixedSize?: string;
|
|
7
8
|
defaultSize?: number;
|
|
8
9
|
minSize?: number;
|
|
9
10
|
maxSize?: number;
|
|
@@ -17,5 +18,6 @@ export interface SectionLayoutProps extends React.ComponentPropsWithoutRef<"div"
|
|
|
17
18
|
orientation?: "horizontal" | "vertical";
|
|
18
19
|
storageKey?: string;
|
|
19
20
|
onSectionResize?: (sectionId: string, newSize: number) => void;
|
|
21
|
+
resizable?: boolean;
|
|
20
22
|
dragHandleColor?: "primary" | "secondary" | "accent" | "border" | "muted";
|
|
21
23
|
}
|
|
@@ -32,3 +32,6 @@ export type { DataTableProps } from './DataTable'
|
|
|
32
32
|
|
|
33
33
|
export { FormReportsSection } from './FormReportsSection'
|
|
34
34
|
export type { FormReportsSectionProps } from './FormReportsSection'
|
|
35
|
+
|
|
36
|
+
export { InboxPanel } from './InboxPanel'
|
|
37
|
+
export type { InboxPanelProps } from './InboxPanel'
|
|
@@ -4,6 +4,7 @@ import { cn } from "@/lib/utils"
|
|
|
4
4
|
export interface AdjustableLayoutSection {
|
|
5
5
|
id: string
|
|
6
6
|
content: React.ReactNode
|
|
7
|
+
fixedSize?: string // CSS size value, e.g. "16rem"
|
|
7
8
|
defaultSize?: number // percentage (0-100)
|
|
8
9
|
minSize?: number // minimum percentage
|
|
9
10
|
maxSize?: number // maximum percentage
|
|
@@ -168,8 +169,20 @@ export const AdjustableLayout = React.memo<AdjustableLayoutProps>(
|
|
|
168
169
|
}, [draggingIndex, handleMouseMove, handleMouseUp])
|
|
169
170
|
|
|
170
171
|
const renderPanel = (section: AdjustableLayoutSection, size: number, index: number) => {
|
|
172
|
+
const nextSection = sections[index + 1]
|
|
171
173
|
// Drag handles should be between panels, so only show for panels that aren't the last one
|
|
172
|
-
const isResizable =
|
|
174
|
+
const isResizable =
|
|
175
|
+
section.resizable !== false &&
|
|
176
|
+
sections.length > 1 &&
|
|
177
|
+
index < sections.length - 1 &&
|
|
178
|
+
!section.fixedSize &&
|
|
179
|
+
!nextSection?.fixedSize
|
|
180
|
+
|
|
181
|
+
const fixedStyle = section.fixedSize
|
|
182
|
+
? orientation === "horizontal"
|
|
183
|
+
? { flex: `0 0 ${section.fixedSize}`, width: section.fixedSize, minWidth: section.fixedSize }
|
|
184
|
+
: { flex: `0 0 ${section.fixedSize}`, height: section.fixedSize, minHeight: section.fixedSize }
|
|
185
|
+
: null
|
|
173
186
|
|
|
174
187
|
return (
|
|
175
188
|
<React.Fragment key={section.id}>
|
|
@@ -179,7 +192,7 @@ export const AdjustableLayout = React.memo<AdjustableLayoutProps>(
|
|
|
179
192
|
section.className
|
|
180
193
|
)}
|
|
181
194
|
style={{
|
|
182
|
-
flex: `${size} 1 0
|
|
195
|
+
...(fixedStyle ?? { flex: `${size} 1 0%` }),
|
|
183
196
|
minHeight: 0,
|
|
184
197
|
minWidth: 0,
|
|
185
198
|
}}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/nextjs-vite"
|
|
2
2
|
import { expect, userEvent, within } from "@storybook/test"
|
|
3
3
|
import { Toaster } from "sonner"
|
|
4
|
+
import { DYNAMIC_TABLE_SCHEMA_VERSION, dynamicTableSchema } from "ui-schema-contracts"
|
|
4
5
|
|
|
5
6
|
import { EnhancedDataTable } from "./EnhancedDataTable"
|
|
6
7
|
import type { DashboardRow } from "./table-types"
|
|
@@ -11,6 +12,22 @@ const rows: DashboardRow[] = [
|
|
|
11
12
|
{ id: 3, header: "Executive summary", type: "Narrative", status: "Done", target: "10", limit: "13", reviewer: "Assign reviewer" },
|
|
12
13
|
]
|
|
13
14
|
|
|
15
|
+
const tableSchema = dynamicTableSchema.parse({
|
|
16
|
+
schemaVersion: DYNAMIC_TABLE_SCHEMA_VERSION,
|
|
17
|
+
rowKey: "id",
|
|
18
|
+
enableFiltering: true,
|
|
19
|
+
enablePagination: true,
|
|
20
|
+
enableRowSelection: true,
|
|
21
|
+
columns: [
|
|
22
|
+
{ key: "header", label: "Header", sortable: true, hideable: false },
|
|
23
|
+
{ key: "type", label: "Type", sortable: true },
|
|
24
|
+
{ key: "status", label: "Status", renderType: "badge", sortable: true },
|
|
25
|
+
{ key: "target", label: "Target", align: "right", sortable: true },
|
|
26
|
+
{ key: "limit", label: "Limit", align: "right", sortable: true },
|
|
27
|
+
{ key: "reviewer", label: "Reviewer", sortable: true },
|
|
28
|
+
],
|
|
29
|
+
})
|
|
30
|
+
|
|
14
31
|
const meta = {
|
|
15
32
|
title: "Composites/EnhancedDataTable/Behaviors",
|
|
16
33
|
component: EnhancedDataTable,
|
|
@@ -30,7 +47,7 @@ export default meta
|
|
|
30
47
|
type Story = StoryObj<typeof meta>
|
|
31
48
|
|
|
32
49
|
export const SelectAllWorks: Story = {
|
|
33
|
-
args: { data: rows },
|
|
50
|
+
args: { data: rows, tableSchema },
|
|
34
51
|
play: async ({ canvasElement }) => {
|
|
35
52
|
const canvas = within(canvasElement)
|
|
36
53
|
const selectAll = canvas.getByLabelText("Select all")
|
|
@@ -40,7 +57,7 @@ export const SelectAllWorks: Story = {
|
|
|
40
57
|
}
|
|
41
58
|
|
|
42
59
|
export const DrawerOpensFromHeader: Story = {
|
|
43
|
-
args: { data: rows },
|
|
60
|
+
args: { data: rows, tableSchema },
|
|
44
61
|
play: async ({ canvasElement }) => {
|
|
45
62
|
const canvas = within(canvasElement)
|
|
46
63
|
const trigger = canvas.getByRole("button", { name: "Cover page" })
|
|
@@ -50,7 +67,7 @@ export const DrawerOpensFromHeader: Story = {
|
|
|
50
67
|
}
|
|
51
68
|
|
|
52
69
|
export const SwitchesViewTab: Story = {
|
|
53
|
-
args: { data: rows },
|
|
70
|
+
args: { data: rows, tableSchema },
|
|
54
71
|
play: async ({ canvasElement }) => {
|
|
55
72
|
const canvas = within(canvasElement)
|
|
56
73
|
const tab = canvas.getByRole("tab", { name: /Past Performance/i })
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/nextjs-vite"
|
|
2
2
|
import { Toaster } from "sonner"
|
|
3
|
+
import { DYNAMIC_TABLE_SCHEMA_VERSION, dynamicTableSchema } from "ui-schema-contracts"
|
|
3
4
|
|
|
4
5
|
import { EnhancedDataTable } from "./EnhancedDataTable"
|
|
5
6
|
import type { DashboardRow } from "./table-types"
|
|
@@ -19,6 +20,22 @@ const data: DashboardRow[] = [
|
|
|
19
20
|
{ id: 12, header: "Advantages Over Current Technologies", type: "Narrative", status: "Not Started", target: "12", limit: "0", reviewer: "Assign reviewer" },
|
|
20
21
|
]
|
|
21
22
|
|
|
23
|
+
const tableSchema = dynamicTableSchema.parse({
|
|
24
|
+
schemaVersion: DYNAMIC_TABLE_SCHEMA_VERSION,
|
|
25
|
+
rowKey: "id",
|
|
26
|
+
enableFiltering: true,
|
|
27
|
+
enablePagination: true,
|
|
28
|
+
enableRowSelection: true,
|
|
29
|
+
columns: [
|
|
30
|
+
{ key: "header", label: "Header", sortable: true, hideable: false },
|
|
31
|
+
{ key: "type", label: "Type", sortable: true },
|
|
32
|
+
{ key: "status", label: "Status", renderType: "badge", sortable: true },
|
|
33
|
+
{ key: "target", label: "Target", align: "right", sortable: true },
|
|
34
|
+
{ key: "limit", label: "Limit", align: "right", sortable: true },
|
|
35
|
+
{ key: "reviewer", label: "Reviewer", sortable: true },
|
|
36
|
+
],
|
|
37
|
+
})
|
|
38
|
+
|
|
22
39
|
const meta = {
|
|
23
40
|
title: "Composites/EnhancedDataTable",
|
|
24
41
|
component: EnhancedDataTable,
|
|
@@ -38,5 +55,6 @@ type Story = StoryObj<typeof meta>
|
|
|
38
55
|
export const Default: Story = {
|
|
39
56
|
args: {
|
|
40
57
|
data,
|
|
58
|
+
tableSchema,
|
|
41
59
|
},
|
|
42
60
|
}
|
|
@@ -504,7 +504,7 @@ export function EnhancedDataTable({
|
|
|
504
504
|
</div>
|
|
505
505
|
|
|
506
506
|
<div className="relative flex min-h-0 flex-1 flex-col gap-4 overflow-hidden px-4 lg:px-6">
|
|
507
|
-
<div className="overflow-x-auto overflow-y-
|
|
507
|
+
<div className="min-h-0 flex-1 overflow-x-auto overflow-y-auto rounded-lg border [scrollbar-width:thin] [scrollbar-color:hsl(var(--border))_transparent] [&::-webkit-scrollbar]:w-2 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-border/70">
|
|
508
508
|
<DndContext
|
|
509
509
|
collisionDetection={closestCenter}
|
|
510
510
|
modifiers={[restrictToVerticalAxis]}
|
|
@@ -565,7 +565,7 @@ export function EnhancedDataTable({
|
|
|
565
565
|
<SelectValue placeholder={currentPageSize} />
|
|
566
566
|
</SelectTrigger>
|
|
567
567
|
<SelectContent side="top">
|
|
568
|
-
{[10, 20, 30, 40, 50].map((pageSize) => (
|
|
568
|
+
{[10, 20, 25, 30, 40, 50].map((pageSize) => (
|
|
569
569
|
<SelectItem key={pageSize} value={`${pageSize}`}>
|
|
570
570
|
{pageSize}
|
|
571
571
|
</SelectItem>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/nextjs-vite"
|
|
2
|
+
|
|
3
|
+
import { InboxList } from "./InboxList"
|
|
4
|
+
|
|
5
|
+
const items = Array.from({ length: 18 }).map((_, index) => ({
|
|
6
|
+
id: `item-${index + 1}`,
|
|
7
|
+
title: `Run ${index + 1}`,
|
|
8
|
+
subtitle: index % 2 === 0 ? "completed • 5s • retries 0" : "running • 23s • retries 1",
|
|
9
|
+
preview: "This is an email-style preview row for validating list readability and vertical scrolling.",
|
|
10
|
+
timestamp: `${index + 1}m ago`,
|
|
11
|
+
badge: index % 2 === 0 ? "completed" : "running",
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
const meta = {
|
|
15
|
+
title: "Composites/InboxList",
|
|
16
|
+
component: InboxList,
|
|
17
|
+
tags: ["autodocs"],
|
|
18
|
+
parameters: {
|
|
19
|
+
layout: "padded",
|
|
20
|
+
},
|
|
21
|
+
} satisfies Meta<typeof InboxList>
|
|
22
|
+
|
|
23
|
+
export default meta
|
|
24
|
+
type Story = StoryObj<typeof meta>
|
|
25
|
+
|
|
26
|
+
export const Default: Story = {
|
|
27
|
+
args: {
|
|
28
|
+
items,
|
|
29
|
+
selectedItemId: "item-2",
|
|
30
|
+
},
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const Empty: Story = {
|
|
34
|
+
args: {
|
|
35
|
+
items: [],
|
|
36
|
+
emptyMessage: "No messages yet.",
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const Loading: Story = {
|
|
41
|
+
args: {
|
|
42
|
+
items: [],
|
|
43
|
+
isLoading: true,
|
|
44
|
+
},
|
|
45
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import * as React from "react"
|
|
2
|
+
|
|
3
|
+
import { Badge } from "@/components/primitives/Badge"
|
|
4
|
+
|
|
5
|
+
export interface InboxListItem {
|
|
6
|
+
id: string
|
|
7
|
+
title: string
|
|
8
|
+
subtitle?: string
|
|
9
|
+
preview?: string
|
|
10
|
+
timestamp?: string
|
|
11
|
+
badge?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface InboxListProps {
|
|
15
|
+
items: InboxListItem[]
|
|
16
|
+
selectedItemId?: string | null
|
|
17
|
+
onSelectItem?: (itemId: string) => void
|
|
18
|
+
isLoading?: boolean
|
|
19
|
+
emptyMessage?: string
|
|
20
|
+
className?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function LoadingRows() {
|
|
24
|
+
return (
|
|
25
|
+
<div className="space-y-2 p-2">
|
|
26
|
+
{Array.from({ length: 8 }).map((_, index) => (
|
|
27
|
+
<div key={index} className="space-y-2 rounded-md border p-3">
|
|
28
|
+
<div className="h-4 w-2/3 animate-pulse rounded bg-muted" />
|
|
29
|
+
<div className="h-3 w-full animate-pulse rounded bg-muted" />
|
|
30
|
+
<div className="h-3 w-1/2 animate-pulse rounded bg-muted" />
|
|
31
|
+
</div>
|
|
32
|
+
))}
|
|
33
|
+
</div>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const InboxList = React.memo<InboxListProps>(
|
|
38
|
+
({
|
|
39
|
+
items,
|
|
40
|
+
selectedItemId,
|
|
41
|
+
onSelectItem,
|
|
42
|
+
isLoading = false,
|
|
43
|
+
emptyMessage = "No items found.",
|
|
44
|
+
className,
|
|
45
|
+
}) => {
|
|
46
|
+
if (isLoading) {
|
|
47
|
+
return (
|
|
48
|
+
<div className={`min-h-0 flex-1 overflow-auto ${className ?? ""}`}>
|
|
49
|
+
<LoadingRows />
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!items.length) {
|
|
55
|
+
return (
|
|
56
|
+
<div className={`flex min-h-0 flex-1 items-center justify-center p-4 text-sm text-muted-foreground ${className ?? ""}`}>
|
|
57
|
+
{emptyMessage}
|
|
58
|
+
</div>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className={`min-h-0 flex-1 overflow-auto ${className ?? ""}`}>
|
|
64
|
+
<div className="space-y-1 p-2">
|
|
65
|
+
{items.map((item) => {
|
|
66
|
+
const isSelected = selectedItemId === item.id
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<button
|
|
70
|
+
key={item.id}
|
|
71
|
+
type="button"
|
|
72
|
+
className={`w-full rounded-md p-3 text-left transition-colors ${
|
|
73
|
+
isSelected
|
|
74
|
+
? "bg-accent ring-1 ring-inset ring-primary"
|
|
75
|
+
: "bg-background hover:bg-accent/60"
|
|
76
|
+
}`}
|
|
77
|
+
onClick={() => onSelectItem?.(item.id)}
|
|
78
|
+
>
|
|
79
|
+
<div className="flex items-center gap-2">
|
|
80
|
+
<div className="truncate font-medium text-sm">{item.title}</div>
|
|
81
|
+
{item.badge ? <Badge variant="outline">{item.badge}</Badge> : null}
|
|
82
|
+
{item.timestamp ? (
|
|
83
|
+
<div className="ml-auto shrink-0 text-muted-foreground text-xs">{item.timestamp}</div>
|
|
84
|
+
) : null}
|
|
85
|
+
</div>
|
|
86
|
+
{item.subtitle ? (
|
|
87
|
+
<div className="mt-1 truncate text-muted-foreground text-xs">{item.subtitle}</div>
|
|
88
|
+
) : null}
|
|
89
|
+
{item.preview ? <div className="mt-2 line-clamp-2 text-sm">{item.preview}</div> : null}
|
|
90
|
+
</button>
|
|
91
|
+
)
|
|
92
|
+
})}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
)
|
|
96
|
+
}
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
InboxList.displayName = "InboxList"
|
|
@@ -6,12 +6,14 @@ import { WorkflowObservabilityFeature } from "./WorkflowObservabilityFeature"
|
|
|
6
6
|
import {
|
|
7
7
|
selectedWorkflowRunMock,
|
|
8
8
|
workflowEventRecordsMock,
|
|
9
|
+
workflowInboxItemsMock,
|
|
9
10
|
workflowSpanRecordsMock,
|
|
10
11
|
workflowStreamRecordsMock,
|
|
11
12
|
} from "./WorkflowObservabilityFeature.mocks"
|
|
12
13
|
|
|
13
14
|
const onSearchQueryChange = fn()
|
|
14
15
|
const onSelectSpan = fn()
|
|
16
|
+
const onSelectInboxItem = fn()
|
|
15
17
|
const onReplayRun = fn()
|
|
16
18
|
const onReenqueue = fn()
|
|
17
19
|
const onCancelActiveSleeps = fn()
|
|
@@ -22,6 +24,11 @@ const meta = {
|
|
|
22
24
|
title: "Features/WorkflowObservabilityFeature/Behaviors",
|
|
23
25
|
component: WorkflowObservabilityFeature,
|
|
24
26
|
tags: ["test"],
|
|
27
|
+
render: (args) => (
|
|
28
|
+
<div className="h-dvh min-h-0 p-2">
|
|
29
|
+
<WorkflowObservabilityFeature {...args} className="h-full" />
|
|
30
|
+
</div>
|
|
31
|
+
),
|
|
25
32
|
globals: {
|
|
26
33
|
theme: "dark-neutral",
|
|
27
34
|
},
|
|
@@ -49,17 +56,28 @@ const args: Story["args"] = {
|
|
|
49
56
|
{ id: "wake-up-sleep", label: "Wake Up Sleep", onClick: onWakeUpSleep, resourceTypes: ["sleep"], tone: "amber", surface: "details" },
|
|
50
57
|
{ id: "cancel-run", label: "Cancel", onClick: onCancel, resourceTypes: ["run"], tone: "danger", surface: "details" },
|
|
51
58
|
],
|
|
59
|
+
inbox: {
|
|
60
|
+
items: workflowInboxItemsMock,
|
|
61
|
+
selectedItemId: selectedWorkflowRunMock.runId,
|
|
62
|
+
onSelectItem: onSelectInboxItem,
|
|
63
|
+
searchQuery: "",
|
|
64
|
+
onSearchQueryChange: fn(),
|
|
65
|
+
},
|
|
66
|
+
className: "h-full",
|
|
52
67
|
}
|
|
53
68
|
|
|
54
69
|
function InteractiveStoryHarness() {
|
|
55
70
|
const [selectedSpanId, setSelectedSpanId] = React.useState<string | null>(null)
|
|
56
71
|
|
|
57
72
|
return (
|
|
58
|
-
<
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
73
|
+
<div className="h-dvh min-h-0 p-2">
|
|
74
|
+
<WorkflowObservabilityFeature
|
|
75
|
+
{...args}
|
|
76
|
+
className="h-full"
|
|
77
|
+
onSelectSpan={setSelectedSpanId}
|
|
78
|
+
selectedSpanId={selectedSpanId}
|
|
79
|
+
/>
|
|
80
|
+
</div>
|
|
63
81
|
)
|
|
64
82
|
}
|
|
65
83
|
|
|
@@ -175,3 +193,14 @@ export const SleepBehavior: Story = {
|
|
|
175
193
|
await expect(await canvas.findByRole("menuitem", { name: /Cancel Active Sleeps/i })).toBeInTheDocument()
|
|
176
194
|
},
|
|
177
195
|
}
|
|
196
|
+
|
|
197
|
+
export const SelectRunFromInbox: Story = {
|
|
198
|
+
args,
|
|
199
|
+
play: async ({ canvasElement }) => {
|
|
200
|
+
const canvas = within(canvasElement)
|
|
201
|
+
|
|
202
|
+
const targetRun = workflowInboxItemsMock[1]
|
|
203
|
+
await userEvent.click(canvas.getByRole("button", { name: new RegExp(targetRun.id, "i") }))
|
|
204
|
+
await expect(onSelectInboxItem).toHaveBeenCalledWith(targetRun.id)
|
|
205
|
+
},
|
|
206
|
+
}
|
package/components/features/WorkflowObservabilityFeature/WorkflowObservabilityFeature.mocks.ts
CHANGED
|
@@ -4,6 +4,26 @@ import type {
|
|
|
4
4
|
WorkflowSpanRecord,
|
|
5
5
|
WorkflowStreamRecord,
|
|
6
6
|
} from "@/components/composites/WorkflowRunObservabilityPanel"
|
|
7
|
+
import type { InboxListItem } from "@/components/composites/InboxList"
|
|
8
|
+
|
|
9
|
+
export const workflowInboxItemsMock: InboxListItem[] = [
|
|
10
|
+
{
|
|
11
|
+
id: "wrun_01KP45XGBHRMT7HQJXXHKBEQS4",
|
|
12
|
+
title: "wrun_01KP45XGBHRMT7HQJXXHKBEQS4",
|
|
13
|
+
subtitle: "running • 1m 43s • retries 0",
|
|
14
|
+
badge: "running",
|
|
15
|
+
timestamp: "1m ago",
|
|
16
|
+
preview: "hook_waiting",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "wrun_01KP45XGBHRMT7HQJXXHKBEQS5",
|
|
20
|
+
title: "wrun_01KP45XGBHRMT7HQJXXHKBEQS5",
|
|
21
|
+
subtitle: "completed • 43s • retries 1",
|
|
22
|
+
badge: "completed",
|
|
23
|
+
timestamp: "7m ago",
|
|
24
|
+
preview: "run_completed",
|
|
25
|
+
},
|
|
26
|
+
]
|
|
7
27
|
|
|
8
28
|
export const selectedWorkflowRunMock: WorkflowRunSummary = {
|
|
9
29
|
runId: "wrun_01KP45XGBHRMT7HQJXXHKBEQS4",
|
package/components/features/WorkflowObservabilityFeature/WorkflowObservabilityFeature.stories.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { WorkflowObservabilityFeature } from "./WorkflowObservabilityFeature"
|
|
|
4
4
|
import {
|
|
5
5
|
selectedWorkflowRunMock,
|
|
6
6
|
workflowEventRecordsMock,
|
|
7
|
+
workflowInboxItemsMock,
|
|
7
8
|
workflowSpanRecordsMock,
|
|
8
9
|
workflowStreamRecordsMock,
|
|
9
10
|
} from "./WorkflowObservabilityFeature.mocks"
|
|
@@ -13,6 +14,11 @@ const meta = {
|
|
|
13
14
|
title: "Features/WorkflowObservabilityFeature",
|
|
14
15
|
component: WorkflowObservabilityFeature,
|
|
15
16
|
tags: ["autodocs"],
|
|
17
|
+
render: (args) => (
|
|
18
|
+
<div className="h-dvh min-h-0 p-2">
|
|
19
|
+
<WorkflowObservabilityFeature {...args} className="h-full" />
|
|
20
|
+
</div>
|
|
21
|
+
),
|
|
16
22
|
globals: {
|
|
17
23
|
theme: "dark-neutral",
|
|
18
24
|
},
|
|
@@ -38,6 +44,12 @@ const baseArgs: Story["args"] = {
|
|
|
38
44
|
{ id: "wake-up-sleep", label: "Wake Up Sleep", resourceTypes: ["sleep"], tone: "amber", surface: "details" },
|
|
39
45
|
{ id: "cancel-run", label: "Cancel", resourceTypes: ["run"], tone: "danger", surface: "details" },
|
|
40
46
|
],
|
|
47
|
+
inbox: {
|
|
48
|
+
items: workflowInboxItemsMock,
|
|
49
|
+
selectedItemId: selectedWorkflowRunMock.runId,
|
|
50
|
+
searchQuery: "",
|
|
51
|
+
},
|
|
52
|
+
className: "h-full",
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
export const Default: Story = {
|
|
@@ -50,17 +62,25 @@ export const WithStateManagement: Story = {
|
|
|
50
62
|
const state = useWorkflowObservabilityFeatureMock()
|
|
51
63
|
|
|
52
64
|
return (
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
65
|
+
<div className="h-dvh min-h-0 p-2">
|
|
66
|
+
<WorkflowObservabilityFeature
|
|
67
|
+
selectedRun={state.selectedRun}
|
|
68
|
+
spans={state.spans}
|
|
69
|
+
events={state.events}
|
|
70
|
+
streams={state.streams}
|
|
71
|
+
selectedSpanId={state.selectedSpanId}
|
|
72
|
+
searchQuery={state.searchQuery}
|
|
73
|
+
runActions={state.runActions}
|
|
74
|
+
onSearchQueryChange={state.actionHandlers?.onSearchQueryChange}
|
|
75
|
+
onSelectSpan={state.actionHandlers?.onSelectSpan}
|
|
76
|
+
inbox={state.inbox ? {
|
|
77
|
+
...state.inbox,
|
|
78
|
+
onSelectItem: state.actionHandlers?.onSelectInboxItem,
|
|
79
|
+
onSearchQueryChange: state.actionHandlers?.onInboxSearchQueryChange,
|
|
80
|
+
} : undefined}
|
|
81
|
+
className="h-full"
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
64
84
|
)
|
|
65
85
|
},
|
|
66
86
|
}
|