@echothink-ui/task 0.1.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.
Files changed (64) hide show
  1. package/README.md +5 -0
  2. package/dist/components/BackendThinkingChain.d.ts +2 -0
  3. package/dist/components/BlockingReasonPanel.d.ts +2 -0
  4. package/dist/components/DAGEdge.d.ts +2 -0
  5. package/dist/components/DAGLegend.d.ts +2 -0
  6. package/dist/components/DAGNode.d.ts +4 -0
  7. package/dist/components/DecisionRequiredPanel.d.ts +2 -0
  8. package/dist/components/HumanInterventionPanel.d.ts +2 -0
  9. package/dist/components/MobileTaskShell.d.ts +12 -0
  10. package/dist/components/TaskApprovalPanel.d.ts +2 -0
  11. package/dist/components/TaskCard.d.ts +2 -0
  12. package/dist/components/TaskDependencyList.d.ts +2 -0
  13. package/dist/components/TaskDetailPanel.d.ts +2 -0
  14. package/dist/components/TaskHandoffPanel.d.ts +2 -0
  15. package/dist/components/TaskProgressIndicator.d.ts +2 -0
  16. package/dist/components/TaskRetryPanel.d.ts +2 -0
  17. package/dist/components/TaskRunLog.d.ts +2 -0
  18. package/dist/components/TaskStatusBadge.d.ts +2 -0
  19. package/dist/components/TaskTable.d.ts +5 -0
  20. package/dist/components/TaskTimeline.d.ts +2 -0
  21. package/dist/components/TaskWaveDAG.d.ts +2 -0
  22. package/dist/components/TaskWaveHeader.d.ts +2 -0
  23. package/dist/components/TaskWaveTable.d.ts +2 -0
  24. package/dist/components/utils.d.ts +13 -0
  25. package/dist/index.cjs +2434 -0
  26. package/dist/index.cjs.map +1 -0
  27. package/dist/index.css +2402 -0
  28. package/dist/index.css.map +1 -0
  29. package/dist/index.d.ts +27 -0
  30. package/dist/index.js +2388 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/types.d.ts +249 -0
  33. package/package.json +45 -0
  34. package/src/components/BackendThinkingChain.tsx +129 -0
  35. package/src/components/BlockingReasonPanel.tsx +67 -0
  36. package/src/components/DAGEdge.tsx +97 -0
  37. package/src/components/DAGLegend.tsx +86 -0
  38. package/src/components/DAGNode.tsx +103 -0
  39. package/src/components/DecisionRequiredPanel.tsx +166 -0
  40. package/src/components/HumanInterventionPanel.tsx +82 -0
  41. package/src/components/MobileTaskShell.tsx +52 -0
  42. package/src/components/TaskApprovalPanel.tsx +159 -0
  43. package/src/components/TaskCard.tsx +71 -0
  44. package/src/components/TaskDependencyList.test.tsx +54 -0
  45. package/src/components/TaskDependencyList.tsx +105 -0
  46. package/src/components/TaskDetailPanel.test.tsx +49 -0
  47. package/src/components/TaskDetailPanel.tsx +139 -0
  48. package/src/components/TaskHandoffPanel.tsx +125 -0
  49. package/src/components/TaskProgressIndicator.tsx +70 -0
  50. package/src/components/TaskRetryPanel.test.tsx +29 -0
  51. package/src/components/TaskRetryPanel.tsx +103 -0
  52. package/src/components/TaskRunLog.tsx +156 -0
  53. package/src/components/TaskStatusBadge.tsx +29 -0
  54. package/src/components/TaskTable.tsx +294 -0
  55. package/src/components/TaskTimeline.tsx +98 -0
  56. package/src/components/TaskWaveDAG.tsx +202 -0
  57. package/src/components/TaskWaveHeader.tsx +82 -0
  58. package/src/components/TaskWaveTable.tsx +151 -0
  59. package/src/components/css.d.ts +1 -0
  60. package/src/components/utils.ts +116 -0
  61. package/src/index.test.tsx +316 -0
  62. package/src/index.tsx +90 -0
  63. package/src/styles.css +2889 -0
  64. package/src/types.ts +289 -0
@@ -0,0 +1,139 @@
1
+ import * as React from "react";
2
+ import clsx from "clsx";
3
+ import { ActionGroup, Badge, EmptyState, Surface } from "@echothink-ui/core";
4
+ import type { TaskDetailPanelProps } from "../types";
5
+ import { TaskDependencyList } from "./TaskDependencyList";
6
+ import { TaskTimeline } from "./TaskTimeline";
7
+ import { TaskStatusBadge } from "./TaskStatusBadge";
8
+ import { formatDateTime, severityForPriority } from "./utils";
9
+
10
+ function formatCount(count: number, singular: string, plural = `${singular}s`) {
11
+ return `${count} ${count === 1 ? singular : plural}`;
12
+ }
13
+
14
+ function DetailSection({
15
+ title,
16
+ meta,
17
+ open,
18
+ children,
19
+ className
20
+ }: {
21
+ title: string;
22
+ meta?: React.ReactNode;
23
+ open?: boolean;
24
+ children: React.ReactNode;
25
+ className?: string;
26
+ }) {
27
+ return (
28
+ <details open={open} className={clsx("eth-task-detail-panel__section", className)}>
29
+ <summary className="eth-task-detail-panel__summary">
30
+ <span className="eth-task-detail-panel__summary-label">{title}</span>
31
+ {meta ? <span className="eth-task-detail-panel__summary-meta">{meta}</span> : null}
32
+ </summary>
33
+ <div className="eth-task-detail-panel__section-body">{children}</div>
34
+ </details>
35
+ );
36
+ }
37
+
38
+ export function TaskDetailPanel({
39
+ task,
40
+ dependencies,
41
+ events,
42
+ actions,
43
+ className,
44
+ title,
45
+ description,
46
+ items,
47
+ ...props
48
+ }: TaskDetailPanelProps) {
49
+ if (!task) {
50
+ return (
51
+ <Surface
52
+ {...props}
53
+ className={clsx("eth-task-detail-panel", className)}
54
+ title={title ?? "Task detail"}
55
+ description={description}
56
+ items={items}
57
+ actions={actions}
58
+ data-eth-component="TaskDetailPanel"
59
+ >
60
+ {!items?.length ? <EmptyState title="No task selected" /> : null}
61
+ </Surface>
62
+ );
63
+ }
64
+
65
+ const resolvedDependencies = dependencies ?? task.dependencies ?? [];
66
+ const resolvedEvents = events ?? task.history ?? [];
67
+
68
+ return (
69
+ <Surface
70
+ {...props}
71
+ className={clsx("eth-task-detail-panel", className)}
72
+ title={title ?? task.title}
73
+ subtitle={task.id}
74
+ status={task.status}
75
+ data-eth-component="TaskDetailPanel"
76
+ >
77
+ <div className="eth-task-detail-panel__stack">
78
+ <DetailSection title="Metadata" open>
79
+ <dl className="eth-meta-grid eth-task-detail-panel__metadata">
80
+ <div>
81
+ <dt>Status</dt>
82
+ <dd>
83
+ <TaskStatusBadge status={task.status} />
84
+ </dd>
85
+ </div>
86
+ <div>
87
+ <dt>Assignee</dt>
88
+ <dd>{task.assignee ?? "-"}</dd>
89
+ </div>
90
+ <div>
91
+ <dt>Due</dt>
92
+ <dd>{formatDateTime(task.dueAt)}</dd>
93
+ </div>
94
+ <div>
95
+ <dt>Priority</dt>
96
+ <dd>
97
+ {task.priority ? (
98
+ <Badge severity={severityForPriority(task.priority)}>{task.priority}</Badge>
99
+ ) : (
100
+ "-"
101
+ )}
102
+ </dd>
103
+ </div>
104
+ </dl>
105
+ </DetailSection>
106
+
107
+ <DetailSection title="Description" open>
108
+ <div className="eth-task-detail-panel__description">
109
+ {task.description ?? description ?? "No description provided."}
110
+ </div>
111
+ </DetailSection>
112
+
113
+ <DetailSection
114
+ title="Dependencies"
115
+ meta={formatCount(resolvedDependencies.length, "dependency", "dependencies")}
116
+ open={resolvedDependencies.length > 0}
117
+ >
118
+ <TaskDependencyList dependencies={resolvedDependencies} />
119
+ </DetailSection>
120
+
121
+ <DetailSection
122
+ title="History"
123
+ meta={formatCount(resolvedEvents.length, "event")}
124
+ open={resolvedEvents.length > 0}
125
+ >
126
+ <TaskTimeline events={resolvedEvents} />
127
+ </DetailSection>
128
+
129
+ {actions?.length ? (
130
+ <DetailSection title="Actions" meta={formatCount(actions.length, "action")} open>
131
+ <div className="eth-task-detail-panel__actions">
132
+ <ActionGroup actions={actions} />
133
+ </div>
134
+ </DetailSection>
135
+ ) : null}
136
+ </div>
137
+ </Surface>
138
+ );
139
+ }
@@ -0,0 +1,125 @@
1
+ import * as React from "react";
2
+ import clsx from "clsx";
3
+ import {
4
+ Badge,
5
+ Button,
6
+ EmptyState,
7
+ FormField,
8
+ Surface,
9
+ Textarea
10
+ } from "@echothink-ui/core";
11
+ import type { TaskHandoffPanelProps } from "../types";
12
+
13
+ type TaskHandoffAssigneeKind =
14
+ NonNullable<TaskHandoffPanelProps["assigneeOptions"]>[number]["kind"];
15
+
16
+ export function TaskHandoffPanel({
17
+ assigneeOptions = [],
18
+ currentAssigneeId,
19
+ onHandoff,
20
+ reasonRequired,
21
+ className,
22
+ title = "Task handoff",
23
+ description,
24
+ ...props
25
+ }: TaskHandoffPanelProps) {
26
+ const [selectedId, setSelectedId] = React.useState(
27
+ currentAssigneeId ?? assigneeOptions[0]?.id ?? ""
28
+ );
29
+ const [reason, setReason] = React.useState("");
30
+ const targetId = React.useId();
31
+ const reasonId = React.useId();
32
+ const trimmedReason = reason.trim();
33
+ const cannotSubmit = !selectedId || (reasonRequired && !trimmedReason);
34
+
35
+ const submitHandoff = (event?: React.FormEvent) => {
36
+ event?.preventDefault();
37
+ if (cannotSubmit) return;
38
+ onHandoff?.(selectedId, trimmedReason);
39
+ };
40
+
41
+ const handleReasonKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
42
+ if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
43
+ event.preventDefault();
44
+ submitHandoff();
45
+ }
46
+ };
47
+
48
+ return (
49
+ <Surface
50
+ {...props}
51
+ className={clsx("eth-task-handoff-panel", className)}
52
+ title={title}
53
+ description={description}
54
+ data-eth-component="TaskHandoffPanel"
55
+ >
56
+ {assigneeOptions.length ? (
57
+ <form className="eth-task-handoff-panel__body" onSubmit={submitHandoff}>
58
+ <div className="eth-task-handoff-panel__target-field">
59
+ <p id={targetId} className="eth-task-handoff-panel__target-heading">
60
+ Handoff target
61
+ </p>
62
+ <div
63
+ className="eth-task-handoff-panel__list"
64
+ role="group"
65
+ aria-labelledby={targetId}
66
+ >
67
+ {assigneeOptions.map((option) => (
68
+ <button
69
+ key={option.id}
70
+ type="button"
71
+ aria-label={`Hand off to ${option.label} (${formatAssigneeKind(
72
+ option.kind
73
+ )})`}
74
+ aria-pressed={selectedId === option.id}
75
+ className={clsx(
76
+ "eth-task-handoff-panel__option",
77
+ selectedId === option.id && "eth-task-handoff-panel__option--selected"
78
+ )}
79
+ onClick={() => setSelectedId(option.id)}
80
+ >
81
+ <span className="eth-task-handoff-panel__option-copy">
82
+ <span className="eth-task-handoff-panel__option-label">{option.label}</span>
83
+ </span>
84
+ <span className="eth-task-handoff-panel__option-meta">
85
+ {selectedId === option.id ? <Badge severity="info">Selected</Badge> : null}
86
+ <Badge severity="neutral">{formatAssigneeKind(option.kind)}</Badge>
87
+ </span>
88
+ </button>
89
+ ))}
90
+ </div>
91
+ </div>
92
+ <FormField
93
+ id={reasonId}
94
+ label="Handoff reason"
95
+ required={reasonRequired}
96
+ className="eth-task-handoff-panel__reason-field"
97
+ helperText={reasonRequired ? "A reason is required for this handoff." : undefined}
98
+ >
99
+ <Textarea
100
+ className="eth-task-handoff-panel__reason"
101
+ name="handoff-reason"
102
+ autoComplete="off"
103
+ placeholder="Summarize context, blockers, and the next action..."
104
+ value={reason}
105
+ rows={3}
106
+ onKeyDown={handleReasonKeyDown}
107
+ onChange={(event) => setReason(event.currentTarget.value)}
108
+ />
109
+ </FormField>
110
+ <div className="eth-task-handoff-panel__actions">
111
+ <Button type="submit" disabled={cannotSubmit}>
112
+ Handoff
113
+ </Button>
114
+ </div>
115
+ </form>
116
+ ) : (
117
+ <EmptyState title="No assignees available" />
118
+ )}
119
+ </Surface>
120
+ );
121
+ }
122
+
123
+ function formatAssigneeKind(kind: TaskHandoffAssigneeKind) {
124
+ return kind.charAt(0).toUpperCase() + kind.slice(1);
125
+ }
@@ -0,0 +1,70 @@
1
+ import * as React from "react";
2
+ import clsx from "clsx";
3
+ import type { TaskProgressIndicatorProps } from "../types";
4
+ import { clampProgress, labelForStatus } from "./utils";
5
+
6
+ const currentStepStatuses = new Set(["active", "in-progress", "running"]);
7
+
8
+ export function TaskProgressIndicator({
9
+ value,
10
+ total = 100,
11
+ steps,
12
+ label = "Task progress",
13
+ className,
14
+ ...props
15
+ }: TaskProgressIndicatorProps) {
16
+ if (steps?.length) {
17
+ return (
18
+ <div
19
+ {...props}
20
+ className={clsx("eth-task-progress eth-task-progress--steps", className)}
21
+ data-eth-component="TaskProgressIndicator"
22
+ >
23
+ <ol className="eth-task-progress__steps" aria-label={label}>
24
+ {steps.map((step, index) => {
25
+ const statusLabel = labelForStatus(step.status);
26
+ const isCurrentStep = currentStepStatuses.has(step.status);
27
+
28
+ return (
29
+ <li
30
+ key={step.id}
31
+ className={clsx(
32
+ "eth-task-progress__step",
33
+ `eth-task-progress__step--${step.status}`
34
+ )}
35
+ aria-current={isCurrentStep ? "step" : undefined}
36
+ data-status={step.status}
37
+ >
38
+ <span className="eth-task-progress__step-track" aria-hidden="true">
39
+ <span className="eth-task-progress__circle" />
40
+ {index < steps.length - 1 ? (
41
+ <span className="eth-task-progress__connector" />
42
+ ) : null}
43
+ </span>
44
+ <span className="eth-task-progress__step-copy">
45
+ <span className="eth-task-progress__step-label">{step.label}</span>
46
+ <span className="eth-task-progress__step-status">{statusLabel}</span>
47
+ </span>
48
+ </li>
49
+ );
50
+ })}
51
+ </ol>
52
+ </div>
53
+ );
54
+ }
55
+
56
+ const progressValue = clampProgress(value, total);
57
+ return (
58
+ <div
59
+ {...props}
60
+ className={clsx("eth-task-progress eth-task-progress--bar", className)}
61
+ data-eth-component="TaskProgressIndicator"
62
+ >
63
+ <div className="eth-task-progress__label">
64
+ <span>{label}</span>
65
+ <strong>{progressValue}%</strong>
66
+ </div>
67
+ <progress value={progressValue} max={100} aria-label={label} />
68
+ </div>
69
+ );
70
+ }
@@ -0,0 +1,29 @@
1
+ import { fireEvent, render, screen } from "@testing-library/react";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { TaskRetryPanel } from "./TaskRetryPanel";
4
+
5
+ describe("TaskRetryPanel", () => {
6
+ it("surfaces exhausted retry policy and submits the captured reason", () => {
7
+ const onRetry = vi.fn();
8
+
9
+ render(
10
+ <TaskRetryPanel
11
+ title="Retry failed task"
12
+ taskRef="Create landing page hero"
13
+ failureSummary="Upstream 503"
14
+ retryPolicy={{ maxRetries: 3, backoff: "exponential", intervalMs: 5000 }}
15
+ attemptCount={3}
16
+ onRetry={onRetry}
17
+ />
18
+ );
19
+
20
+ expect(screen.getByText("Automatic retries exhausted")).toBeTruthy();
21
+
22
+ fireEvent.change(screen.getByLabelText("Retry reason"), {
23
+ target: { value: "Renderer recovered after queue drain." }
24
+ });
25
+ fireEvent.click(screen.getByRole("button", { name: "Retry task" }));
26
+
27
+ expect(onRetry).toHaveBeenCalledWith("Renderer recovered after queue drain.");
28
+ });
29
+ });
@@ -0,0 +1,103 @@
1
+ import * as React from "react";
2
+ import clsx from "clsx";
3
+ import { Button, FormField, InlineNotification, Surface, Textarea } from "@echothink-ui/core";
4
+ import { RetryIcon } from "@echothink-ui/icons";
5
+ import type { TaskRetryPanelProps } from "../types";
6
+ import { formatDuration } from "./utils";
7
+
8
+ const backoffLabels = {
9
+ exponential: "Exponential",
10
+ linear: "Linear"
11
+ } as const;
12
+
13
+ export function TaskRetryPanel({
14
+ taskRef,
15
+ failureSummary,
16
+ retryPolicy,
17
+ attemptCount = 0,
18
+ onRetry,
19
+ className,
20
+ title = "Retry failed task",
21
+ description,
22
+ ...props
23
+ }: TaskRetryPanelProps) {
24
+ const [reason, setReason] = React.useState("");
25
+ const reasonId = React.useId();
26
+ const retriesExhausted = retryPolicy ? attemptCount >= retryPolicy.maxRetries : false;
27
+ const attemptLabel = retryPolicy ? `${attemptCount} / ${retryPolicy.maxRetries}` : attemptCount;
28
+ const backoffLabel = retryPolicy ? backoffLabels[retryPolicy.backoff] : "-";
29
+
30
+ return (
31
+ <Surface
32
+ {...props}
33
+ className={clsx("eth-task-retry-panel", className)}
34
+ title={title}
35
+ description={description}
36
+ severity="danger"
37
+ data-eth-component="TaskRetryPanel"
38
+ >
39
+ <InlineNotification severity="danger" title="Task failed">
40
+ {failureSummary ?? "The task did not complete successfully."}
41
+ </InlineNotification>
42
+ <dl className="eth-task-retry-panel__policy" aria-label="Retry policy">
43
+ <div>
44
+ <dt>Task</dt>
45
+ <dd>{taskRef ?? "-"}</dd>
46
+ </div>
47
+ <div
48
+ className={clsx(
49
+ "eth-task-retry-panel__attempts",
50
+ retriesExhausted && "eth-task-retry-panel__attempts--exhausted"
51
+ )}
52
+ >
53
+ <dt>Attempts</dt>
54
+ <dd>
55
+ <span>{attemptLabel}</span>
56
+ {retriesExhausted ? (
57
+ <span className="eth-task-retry-panel__policy-note">
58
+ Automatic retries exhausted
59
+ </span>
60
+ ) : null}
61
+ </dd>
62
+ </div>
63
+ {retryPolicy ? (
64
+ <>
65
+ <div>
66
+ <dt>Backoff</dt>
67
+ <dd>{backoffLabel}</dd>
68
+ </div>
69
+ <div>
70
+ <dt>Interval</dt>
71
+ <dd>{formatDuration(retryPolicy.intervalMs)}</dd>
72
+ </div>
73
+ </>
74
+ ) : null}
75
+ </dl>
76
+ <div className="eth-task-retry-panel__controls">
77
+ <FormField
78
+ id={reasonId}
79
+ label="Retry reason"
80
+ helperText="Stored with retry audit metadata."
81
+ className="eth-task-retry-panel__reason-field"
82
+ >
83
+ <Textarea
84
+ value={reason}
85
+ rows={3}
86
+ placeholder="Summarize what changed before retrying."
87
+ onChange={(event) => setReason(event.currentTarget.value)}
88
+ />
89
+ </FormField>
90
+ <div className="eth-task-retry-panel__actions">
91
+ <Button
92
+ icon={<RetryIcon size={16} />}
93
+ intent="primary"
94
+ onClick={() => onRetry?.(reason)}
95
+ >
96
+ Retry task
97
+ </Button>
98
+ <p>Creates a new attempt and records this reason in the task audit trail.</p>
99
+ </div>
100
+ </div>
101
+ </Surface>
102
+ );
103
+ }
@@ -0,0 +1,156 @@
1
+ import * as React from "react";
2
+ import clsx from "clsx";
3
+ import { Badge, Checkbox, EmptyState, IconButton, Surface } from "@echothink-ui/core";
4
+ import { PauseIcon, PlayIcon } from "@echothink-ui/icons";
5
+ import type { TaskRunLogEntry, TaskRunLogProps } from "../types";
6
+ import { formatDateTime } from "./utils";
7
+
8
+ const logLevels: TaskRunLogEntry["level"][] = ["error", "warn", "info", "debug"];
9
+
10
+ export function TaskRunLog({
11
+ entries = [],
12
+ streaming,
13
+ onPauseToggle,
14
+ onFilter,
15
+ levelFilter,
16
+ windowSize = 200,
17
+ className,
18
+ title = "Task run log",
19
+ description,
20
+ ...props
21
+ }: TaskRunLogProps) {
22
+ const [internalFilter, setInternalFilter] = React.useState<TaskRunLogEntry["level"][]>(logLevels);
23
+ const activeFilter = (levelFilter as TaskRunLogEntry["level"][] | undefined) ?? internalFilter;
24
+ const counts = React.useMemo(
25
+ () =>
26
+ logLevels.reduce(
27
+ (summary, level) => ({
28
+ ...summary,
29
+ [level]: entries.filter((entry) => entry.level === level).length
30
+ }),
31
+ {} as Record<TaskRunLogEntry["level"], number>
32
+ ),
33
+ [entries]
34
+ );
35
+ const visibleEntries = React.useMemo(
36
+ () =>
37
+ entries
38
+ .filter((entry) => activeFilter.includes(entry.level))
39
+ .slice(-windowSize),
40
+ [activeFilter, entries, windowSize]
41
+ );
42
+
43
+ const toggleLevel = (level: TaskRunLogEntry["level"]) => {
44
+ onFilter?.(level);
45
+ if (!levelFilter) {
46
+ setInternalFilter((current) =>
47
+ current.includes(level)
48
+ ? current.filter((candidate) => candidate !== level)
49
+ : [...current, level]
50
+ );
51
+ }
52
+ };
53
+ const streamLabel = streaming ? "Streaming" : "Paused";
54
+
55
+ return (
56
+ <Surface
57
+ {...props}
58
+ className={clsx("eth-task-run-log", className)}
59
+ title={title}
60
+ description={description}
61
+ data-eth-component="TaskRunLog"
62
+ >
63
+ <div className="eth-task-run-log__toolbar" role="toolbar" aria-label="Task log controls">
64
+ <div className="eth-task-run-log__stream-state" aria-live="polite">
65
+ {onPauseToggle ? (
66
+ <IconButton
67
+ label={streaming ? "Pause log stream" : "Resume log stream"}
68
+ icon={streaming ? <PauseIcon size={16} /> : <PlayIcon size={16} />}
69
+ intent="ghost"
70
+ density="compact"
71
+ onClick={onPauseToggle}
72
+ />
73
+ ) : null}
74
+ <Badge severity={streaming ? "success" : "neutral"}>{streamLabel}</Badge>
75
+ <span className="eth-task-run-log__buffer-count">
76
+ {visibleEntries.length} of {entries.length} entries
77
+ </span>
78
+ </div>
79
+ <fieldset className="eth-task-run-log__filters">
80
+ <legend>Levels</legend>
81
+ <div className="eth-task-run-log__filter-options">
82
+ {logLevels.map((level) => (
83
+ <Checkbox
84
+ key={level}
85
+ label={level}
86
+ checked={activeFilter.includes(level)}
87
+ onChange={() => toggleLevel(level)}
88
+ />
89
+ ))}
90
+ </div>
91
+ </fieldset>
92
+ </div>
93
+ <dl className="eth-task-run-log__summary" aria-label="Task log summary">
94
+ <div>
95
+ <dt>Buffered</dt>
96
+ <dd>{entries.length}</dd>
97
+ </div>
98
+ <div>
99
+ <dt>Visible</dt>
100
+ <dd>{visibleEntries.length}</dd>
101
+ </div>
102
+ <div>
103
+ <dt>Warnings</dt>
104
+ <dd>{counts.warn}</dd>
105
+ </div>
106
+ <div>
107
+ <dt>Errors</dt>
108
+ <dd>{counts.error}</dd>
109
+ </div>
110
+ </dl>
111
+ {visibleEntries.length ? (
112
+ <div
113
+ className="eth-task-run-log__viewport"
114
+ role="log"
115
+ aria-label="Task execution log entries"
116
+ aria-live={streaming ? "polite" : "off"}
117
+ aria-relevant="additions text"
118
+ >
119
+ <div className="eth-task-run-log__columns" aria-hidden="true">
120
+ <span>Time</span>
121
+ <span>Level</span>
122
+ <span>Message</span>
123
+ </div>
124
+ {visibleEntries.map((entry) => (
125
+ <div
126
+ key={entry.id}
127
+ className={clsx("eth-task-run-log__entry", `eth-task-run-log__entry--${entry.level}`)}
128
+ >
129
+ <time className="eth-task-run-log__time" dateTime={String(entry.timestamp)}>
130
+ {formatDateTime(entry.timestamp)}
131
+ </time>
132
+ <span
133
+ className={clsx(
134
+ "eth-task-run-log__level",
135
+ `eth-task-run-log__level--${entry.level}`
136
+ )}
137
+ >
138
+ {entry.level.toUpperCase()}
139
+ </span>
140
+ <span
141
+ className={clsx(
142
+ "eth-task-run-log__message",
143
+ entry.redacted && "eth-task-run-log__message--redacted"
144
+ )}
145
+ >
146
+ {entry.redacted ? "[redacted]" : entry.message}
147
+ </span>
148
+ </div>
149
+ ))}
150
+ </div>
151
+ ) : (
152
+ <EmptyState title="No log entries" />
153
+ )}
154
+ </Surface>
155
+ );
156
+ }
@@ -0,0 +1,29 @@
1
+ import * as React from "react";
2
+ import clsx from "clsx";
3
+ import { Badge } from "@echothink-ui/core";
4
+ import type { TaskStatusBadgeProps } from "../types";
5
+ import { labelForStatus, severityForStatus } from "./utils";
6
+
7
+ export function TaskStatusBadge({
8
+ status,
9
+ label,
10
+ className,
11
+ title,
12
+ ...props
13
+ }: TaskStatusBadgeProps) {
14
+ const statusLabel = label ?? labelForStatus(status);
15
+
16
+ return (
17
+ <Badge
18
+ {...props}
19
+ className={clsx("eth-task-status-badge", `eth-task-status-badge--${status}`, className)}
20
+ data-eth-component="TaskStatusBadge"
21
+ data-status={status}
22
+ severity={severityForStatus(status)}
23
+ title={title ?? statusLabel}
24
+ >
25
+ <span className="eth-task-status-badge__indicator" aria-hidden="true" />
26
+ <span className="eth-task-status-badge__label">{statusLabel}</span>
27
+ </Badge>
28
+ );
29
+ }