@echothink-ui/developer 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.
- package/README.md +5 -0
- package/dist/components/APIExplorer.d.ts +2 -0
- package/dist/components/BranchSelector.d.ts +2 -0
- package/dist/components/CodeBlock.d.ts +2 -0
- package/dist/components/CodeEditor.d.ts +2 -0
- package/dist/components/CommitList.d.ts +2 -0
- package/dist/components/DiffTable.d.ts +2 -0
- package/dist/components/DiffViewer.d.ts +2 -0
- package/dist/components/EventPayloadViewer.d.ts +2 -0
- package/dist/components/GitRepositoryPanel.d.ts +2 -0
- package/dist/components/JSONViewer.d.ts +2 -0
- package/dist/components/LogConsole.d.ts +2 -0
- package/dist/components/PullRequestPanel.d.ts +2 -0
- package/dist/components/RequestResponseViewer.d.ts +2 -0
- package/dist/components/SchemaViewer.d.ts +2 -0
- package/dist/components/TerminalPanel.d.ts +2 -0
- package/dist/components/TraceTimeline.d.ts +2 -0
- package/dist/components/WebhookEventViewer.d.ts +2 -0
- package/dist/components/YAMLViewer.d.ts +2 -0
- package/dist/components/devUtils.d.ts +10 -0
- package/dist/components/types.d.ts +196 -0
- package/dist/index.cjs +2627 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.css +3651 -0
- package/dist/index.css.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +2572 -0
- package/dist/index.js.map +1 -0
- package/package.json +43 -0
- package/src/components/APIExplorer.tsx +205 -0
- package/src/components/BranchSelector.tsx +54 -0
- package/src/components/CodeBlock.tsx +127 -0
- package/src/components/CodeEditor.tsx +95 -0
- package/src/components/CommitList.tsx +100 -0
- package/src/components/DiffTable.tsx +288 -0
- package/src/components/DiffViewer.tsx +145 -0
- package/src/components/EventPayloadViewer.tsx +91 -0
- package/src/components/GitRepositoryPanel.tsx +73 -0
- package/src/components/JSONViewer.tsx +189 -0
- package/src/components/LogConsole.tsx +160 -0
- package/src/components/PullRequestPanel.test.tsx +52 -0
- package/src/components/PullRequestPanel.tsx +215 -0
- package/src/components/RequestResponseViewer.test.tsx +45 -0
- package/src/components/RequestResponseViewer.tsx +169 -0
- package/src/components/SchemaViewer.tsx +157 -0
- package/src/components/TerminalPanel.test.tsx +33 -0
- package/src/components/TerminalPanel.tsx +134 -0
- package/src/components/TraceTimeline.test.tsx +63 -0
- package/src/components/TraceTimeline.tsx +207 -0
- package/src/components/WebhookEventViewer.test.tsx +57 -0
- package/src/components/WebhookEventViewer.tsx +184 -0
- package/src/components/YAMLViewer.tsx +207 -0
- package/src/components/devUtils.ts +81 -0
- package/src/components/types.ts +230 -0
- package/src/index.tsx +72 -0
- package/src/styles.css +4296 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Button, FormField, Select, TextInput, Textarea } from "@echothink-ui/core";
|
|
3
|
+
import type { APIEndpoint, APIExplorerProps, APIRequest } from "./types";
|
|
4
|
+
import { formatUnknown } from "./devUtils";
|
|
5
|
+
|
|
6
|
+
type ResponseState =
|
|
7
|
+
| { status: "idle" }
|
|
8
|
+
| { status: "loading" }
|
|
9
|
+
| { status: "success"; value: unknown }
|
|
10
|
+
| { status: "error"; value: unknown };
|
|
11
|
+
|
|
12
|
+
export function APIExplorer({
|
|
13
|
+
endpoints,
|
|
14
|
+
selectedId,
|
|
15
|
+
onSelect,
|
|
16
|
+
onSend,
|
|
17
|
+
className,
|
|
18
|
+
...props
|
|
19
|
+
}: APIExplorerProps) {
|
|
20
|
+
const [internalId, setInternalId] = React.useState(selectedId ?? endpoints[0]?.id);
|
|
21
|
+
const selected =
|
|
22
|
+
endpoints.find((endpoint) => endpoint.id === (selectedId ?? internalId)) ?? endpoints[0];
|
|
23
|
+
const [method, setMethod] = React.useState(selected?.method ?? "GET");
|
|
24
|
+
const [path, setPath] = React.useState(selected?.path ?? "");
|
|
25
|
+
const [headers, setHeaders] = React.useState(() => formatEndpointValue(selected?.headers, "{}"));
|
|
26
|
+
const [body, setBody] = React.useState(() => formatEndpointValue(selected?.body, ""));
|
|
27
|
+
const [response, setResponse] = React.useState<ResponseState>({ status: "idle" });
|
|
28
|
+
const headerError = validateHeaderJson(headers);
|
|
29
|
+
|
|
30
|
+
React.useEffect(() => {
|
|
31
|
+
if (!selected) return;
|
|
32
|
+
setMethod(selected.method);
|
|
33
|
+
setPath(selected.path);
|
|
34
|
+
setHeaders(formatEndpointValue(selected.headers, "{}"));
|
|
35
|
+
setBody(formatEndpointValue(selected.body, ""));
|
|
36
|
+
}, [selected?.id]);
|
|
37
|
+
|
|
38
|
+
const choose = (endpoint: APIEndpoint) => {
|
|
39
|
+
setInternalId(endpoint.id);
|
|
40
|
+
onSelect?.(endpoint.id);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const send = async () => {
|
|
44
|
+
if (headerError || !path.trim()) return;
|
|
45
|
+
|
|
46
|
+
const request: APIRequest = { endpointId: selected?.id, method, path, headers, body };
|
|
47
|
+
setResponse({ status: "loading" });
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
const result = await onSend?.(request);
|
|
51
|
+
setResponse({ status: "success", value: result ?? { ok: true, request } });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
setResponse({ status: "error", value: formatError(error) });
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const isLoading = response.status === "loading";
|
|
58
|
+
const responseStateLabel = response.status === "idle" ? "Ready" : response.status;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<section
|
|
62
|
+
{...props}
|
|
63
|
+
className={`eth-dev-api-explorer ${className ?? ""}`}
|
|
64
|
+
data-eth-component="APIExplorer"
|
|
65
|
+
>
|
|
66
|
+
<nav className="eth-dev-api-explorer__nav" aria-label="Endpoints">
|
|
67
|
+
<div className="eth-dev-api-explorer__nav-heading">
|
|
68
|
+
<span>Endpoints</span>
|
|
69
|
+
<span>{endpoints.length}</span>
|
|
70
|
+
</div>
|
|
71
|
+
<div className="eth-dev-api-explorer__endpoint-list">
|
|
72
|
+
{endpoints.length ? (
|
|
73
|
+
endpoints.map((endpoint) => (
|
|
74
|
+
<button
|
|
75
|
+
key={endpoint.id}
|
|
76
|
+
type="button"
|
|
77
|
+
aria-current={selected?.id === endpoint.id}
|
|
78
|
+
onClick={() => choose(endpoint)}
|
|
79
|
+
>
|
|
80
|
+
<span
|
|
81
|
+
className={`eth-dev-api-explorer__method eth-dev-api-explorer__method--${methodToken(endpoint.method)}`}
|
|
82
|
+
>
|
|
83
|
+
{endpoint.method}
|
|
84
|
+
</span>
|
|
85
|
+
<span className="eth-dev-api-explorer__endpoint-copy">
|
|
86
|
+
<code>{endpoint.path}</code>
|
|
87
|
+
{endpoint.summary ? <small>{endpoint.summary}</small> : null}
|
|
88
|
+
</span>
|
|
89
|
+
</button>
|
|
90
|
+
))
|
|
91
|
+
) : (
|
|
92
|
+
<p className="eth-dev-api-explorer__empty">No endpoints configured.</p>
|
|
93
|
+
)}
|
|
94
|
+
</div>
|
|
95
|
+
</nav>
|
|
96
|
+
<form
|
|
97
|
+
className="eth-dev-api-explorer__request"
|
|
98
|
+
onSubmit={(event) => {
|
|
99
|
+
event.preventDefault();
|
|
100
|
+
void send();
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
<header className="eth-dev-api-explorer__request-header">
|
|
104
|
+
<div>
|
|
105
|
+
<p>Request</p>
|
|
106
|
+
<h3>{selected?.summary ?? "Custom endpoint"}</h3>
|
|
107
|
+
</div>
|
|
108
|
+
{selected ? <code>{selected.id}</code> : null}
|
|
109
|
+
</header>
|
|
110
|
+
<div className="eth-dev-api-explorer__method-path">
|
|
111
|
+
<FormField label="Method">
|
|
112
|
+
<Select
|
|
113
|
+
value={method}
|
|
114
|
+
options={["GET", "POST", "PUT", "PATCH", "DELETE"].map((item) => ({
|
|
115
|
+
value: item,
|
|
116
|
+
label: item
|
|
117
|
+
}))}
|
|
118
|
+
onChange={(event) => setMethod(event.currentTarget.value)}
|
|
119
|
+
/>
|
|
120
|
+
</FormField>
|
|
121
|
+
<FormField label="Path">
|
|
122
|
+
<TextInput value={path} onChange={(event) => setPath(event.currentTarget.value)} />
|
|
123
|
+
</FormField>
|
|
124
|
+
</div>
|
|
125
|
+
<FormField label="Headers" error={headerError}>
|
|
126
|
+
<Textarea
|
|
127
|
+
className="eth-dev-api-explorer__code-field"
|
|
128
|
+
value={headers}
|
|
129
|
+
onChange={(event) => setHeaders(event.currentTarget.value)}
|
|
130
|
+
rows={5}
|
|
131
|
+
/>
|
|
132
|
+
</FormField>
|
|
133
|
+
<FormField label="Body">
|
|
134
|
+
<Textarea
|
|
135
|
+
className="eth-dev-api-explorer__code-field"
|
|
136
|
+
value={body}
|
|
137
|
+
onChange={(event) => setBody(event.currentTarget.value)}
|
|
138
|
+
rows={8}
|
|
139
|
+
/>
|
|
140
|
+
</FormField>
|
|
141
|
+
<div className="eth-dev-api-explorer__actions">
|
|
142
|
+
<span>
|
|
143
|
+
{method} {path || "/"}
|
|
144
|
+
</span>
|
|
145
|
+
<Button type="submit" loading={isLoading} disabled={Boolean(headerError) || !path.trim()}>
|
|
146
|
+
Send
|
|
147
|
+
</Button>
|
|
148
|
+
</div>
|
|
149
|
+
</form>
|
|
150
|
+
<section className="eth-dev-api-explorer__response" aria-live="polite" aria-busy={isLoading}>
|
|
151
|
+
<header className="eth-dev-api-explorer__response-header">
|
|
152
|
+
<h3>Response</h3>
|
|
153
|
+
<span
|
|
154
|
+
className={`eth-dev-api-explorer__response-state eth-dev-api-explorer__response-state--${response.status}`}
|
|
155
|
+
>
|
|
156
|
+
{responseStateLabel}
|
|
157
|
+
</span>
|
|
158
|
+
</header>
|
|
159
|
+
{response.status === "idle" ? (
|
|
160
|
+
<div className="eth-dev-api-explorer__response-empty">No response yet.</div>
|
|
161
|
+
) : response.status === "loading" ? (
|
|
162
|
+
<div className="eth-dev-api-explorer__response-empty">Waiting for response.</div>
|
|
163
|
+
) : (
|
|
164
|
+
<pre>
|
|
165
|
+
<code>{formatUnknown(response.value)}</code>
|
|
166
|
+
</pre>
|
|
167
|
+
)}
|
|
168
|
+
</section>
|
|
169
|
+
</section>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function formatEndpointValue(value: unknown, fallback: string) {
|
|
174
|
+
if (value == null) return fallback;
|
|
175
|
+
if (typeof value === "string") return value;
|
|
176
|
+
return formatUnknown(value);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function validateHeaderJson(value: string) {
|
|
180
|
+
const trimmed = value.trim();
|
|
181
|
+
if (!trimmed) return null;
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
const parsed = JSON.parse(trimmed);
|
|
185
|
+
if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") {
|
|
186
|
+
return "Headers must be a JSON object.";
|
|
187
|
+
}
|
|
188
|
+
} catch {
|
|
189
|
+
return "Headers must be valid JSON.";
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function formatError(error: unknown) {
|
|
196
|
+
if (error instanceof Error) {
|
|
197
|
+
return { error: error.message };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return error;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function methodToken(method: string) {
|
|
204
|
+
return method.toLowerCase().replace(/[^a-z0-9-]/g, "");
|
|
205
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Select } from "@echothink-ui/core";
|
|
3
|
+
import type { BranchSelectorProps } from "./types";
|
|
4
|
+
|
|
5
|
+
export function BranchSelector({
|
|
6
|
+
branches,
|
|
7
|
+
value,
|
|
8
|
+
onChange,
|
|
9
|
+
className,
|
|
10
|
+
disabled,
|
|
11
|
+
labelText = "Branch",
|
|
12
|
+
helperText,
|
|
13
|
+
density = "compact",
|
|
14
|
+
...props
|
|
15
|
+
}: BranchSelectorProps) {
|
|
16
|
+
const hasBranches = branches.length > 0;
|
|
17
|
+
const branchCountLabel =
|
|
18
|
+
branches.length === 1 ? "1 branch available" : `${branches.length} branches available`;
|
|
19
|
+
const resolvedHelperText =
|
|
20
|
+
helperText === undefined
|
|
21
|
+
? hasBranches
|
|
22
|
+
? branchCountLabel
|
|
23
|
+
: "No branches available"
|
|
24
|
+
: helperText;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<Select
|
|
28
|
+
{...props}
|
|
29
|
+
className={[
|
|
30
|
+
"eth-dev-branch-selector",
|
|
31
|
+
!hasBranches && "eth-dev-branch-selector--empty",
|
|
32
|
+
className
|
|
33
|
+
]
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.join(" ")}
|
|
36
|
+
data-eth-component="BranchSelector"
|
|
37
|
+
density={density}
|
|
38
|
+
disabled={disabled || !hasBranches}
|
|
39
|
+
helperText={resolvedHelperText}
|
|
40
|
+
labelText={labelText}
|
|
41
|
+
value={hasBranches ? value : ""}
|
|
42
|
+
options={
|
|
43
|
+
hasBranches
|
|
44
|
+
? branches.map((branch) => ({ value: branch, label: branch }))
|
|
45
|
+
: [{ value: "", label: "No branches available", disabled: true }]
|
|
46
|
+
}
|
|
47
|
+
onChange={(event) => {
|
|
48
|
+
if (hasBranches) {
|
|
49
|
+
onChange?.(event.currentTarget.value);
|
|
50
|
+
}
|
|
51
|
+
}}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Checkmark, Copy } from "@carbon/icons-react";
|
|
3
|
+
import { IconButton } from "@echothink-ui/core";
|
|
4
|
+
import type { CodeBlockProps } from "./types";
|
|
5
|
+
import { splitLines } from "./devUtils";
|
|
6
|
+
|
|
7
|
+
const keywordPattern =
|
|
8
|
+
/\b(?:async|await|break|case|catch|class|const|continue|default|else|export|extends|false|finally|for|from|function|if|import|interface|let|new|null|return|throw|true|try|type|undefined|var|while)\b/;
|
|
9
|
+
const tokenPattern =
|
|
10
|
+
/\/\/.*|\/\*.*?\*\/|"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|`(?:\\.|[^`\\])*`|\b\d+(?:\.\d+)?\b|\b(?:async|await|break|case|catch|class|const|continue|default|else|export|extends|false|finally|for|from|function|if|import|interface|let|new|null|return|throw|true|try|type|undefined|var|while)\b/g;
|
|
11
|
+
|
|
12
|
+
function getTokenKind(token: string) {
|
|
13
|
+
if (token.startsWith("//") || token.startsWith("/*")) return "comment";
|
|
14
|
+
if (token.startsWith('"') || token.startsWith("'") || token.startsWith("`")) return "string";
|
|
15
|
+
if (/^\d/.test(token)) return "number";
|
|
16
|
+
if (keywordPattern.test(token)) return "keyword";
|
|
17
|
+
return "plain";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function renderCodeLine(line: string) {
|
|
21
|
+
const parts: React.ReactNode[] = [];
|
|
22
|
+
let lastIndex = 0;
|
|
23
|
+
tokenPattern.lastIndex = 0;
|
|
24
|
+
for (let match = tokenPattern.exec(line); match; match = tokenPattern.exec(line)) {
|
|
25
|
+
const token = match[0];
|
|
26
|
+
if (match.index > lastIndex) {
|
|
27
|
+
parts.push(line.slice(lastIndex, match.index));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const kind = getTokenKind(token);
|
|
31
|
+
parts.push(
|
|
32
|
+
<span
|
|
33
|
+
key={`${match.index}-${token}`}
|
|
34
|
+
className={`eth-dev-code-block__token eth-dev-code-block__token--${kind}`}
|
|
35
|
+
>
|
|
36
|
+
{token}
|
|
37
|
+
</span>
|
|
38
|
+
);
|
|
39
|
+
lastIndex = match.index + token.length;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (lastIndex < line.length) {
|
|
43
|
+
parts.push(line.slice(lastIndex));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return parts.length ? parts : line;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function CodeBlock({
|
|
50
|
+
code,
|
|
51
|
+
language = "text",
|
|
52
|
+
showLineNumbers = false,
|
|
53
|
+
className,
|
|
54
|
+
...props
|
|
55
|
+
}: CodeBlockProps) {
|
|
56
|
+
const [copied, setCopied] = React.useState(false);
|
|
57
|
+
const resetTimer = React.useRef<number | undefined>(undefined);
|
|
58
|
+
const lines = splitLines(code);
|
|
59
|
+
|
|
60
|
+
React.useEffect(() => {
|
|
61
|
+
return () => {
|
|
62
|
+
if (resetTimer.current !== undefined && typeof window !== "undefined") {
|
|
63
|
+
window.clearTimeout(resetTimer.current);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
}, []);
|
|
67
|
+
|
|
68
|
+
const copy = React.useCallback(() => {
|
|
69
|
+
if (typeof navigator === "undefined" || !navigator.clipboard) return;
|
|
70
|
+
|
|
71
|
+
void navigator.clipboard
|
|
72
|
+
.writeText(code)
|
|
73
|
+
.then(() => {
|
|
74
|
+
if (typeof window !== "undefined") {
|
|
75
|
+
setCopied(true);
|
|
76
|
+
if (resetTimer.current !== undefined) {
|
|
77
|
+
window.clearTimeout(resetTimer.current);
|
|
78
|
+
}
|
|
79
|
+
resetTimer.current = window.setTimeout(() => setCopied(false), 1200);
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
.catch(() => {
|
|
83
|
+
setCopied(false);
|
|
84
|
+
});
|
|
85
|
+
}, [code]);
|
|
86
|
+
|
|
87
|
+
const normalizedLanguage = language.trim() || "text";
|
|
88
|
+
const languageClass = normalizedLanguage.toLowerCase().replace(/[^a-z0-9_-]/g, "-");
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<section
|
|
92
|
+
{...props}
|
|
93
|
+
className={`eth-dev-code-block ${showLineNumbers ? "eth-dev-code-block--numbered" : ""} ${className ?? ""}`}
|
|
94
|
+
data-eth-component="CodeBlock"
|
|
95
|
+
data-language={normalizedLanguage}
|
|
96
|
+
>
|
|
97
|
+
<header className="eth-dev-code-block__header">
|
|
98
|
+
<span className="eth-dev-code-block__language">{normalizedLanguage}</span>
|
|
99
|
+
<IconButton
|
|
100
|
+
className="eth-dev-code-block__copy-button"
|
|
101
|
+
label={copied ? "Copied" : "Copy code"}
|
|
102
|
+
intent="ghost"
|
|
103
|
+
density="compact"
|
|
104
|
+
icon={copied ? <Checkmark size={16} /> : <Copy size={16} />}
|
|
105
|
+
onClick={copy}
|
|
106
|
+
/>
|
|
107
|
+
<span className="eth-dev-code-block__copy-status" aria-live="polite">
|
|
108
|
+
{copied ? "Code copied" : ""}
|
|
109
|
+
</span>
|
|
110
|
+
</header>
|
|
111
|
+
<pre className={`eth-dev-code-block__pre language-${languageClass}`}>
|
|
112
|
+
<code className="eth-dev-code-block__code">
|
|
113
|
+
{lines.map((line, index) => (
|
|
114
|
+
<span key={index} className="eth-dev-code-block__line">
|
|
115
|
+
{showLineNumbers ? (
|
|
116
|
+
<span className="eth-dev-code-block__gutter" aria-hidden>
|
|
117
|
+
{index + 1}
|
|
118
|
+
</span>
|
|
119
|
+
) : null}
|
|
120
|
+
<span className="eth-dev-code-block__line-code">{renderCodeLine(line)}</span>
|
|
121
|
+
</span>
|
|
122
|
+
))}
|
|
123
|
+
</code>
|
|
124
|
+
</pre>
|
|
125
|
+
</section>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import type { CodeEditorProps } from "./types";
|
|
3
|
+
import { splitLines } from "./devUtils";
|
|
4
|
+
|
|
5
|
+
export function CodeEditor({
|
|
6
|
+
value,
|
|
7
|
+
onChange,
|
|
8
|
+
language = "text",
|
|
9
|
+
readonly,
|
|
10
|
+
readOnly,
|
|
11
|
+
schemaRef,
|
|
12
|
+
className,
|
|
13
|
+
disabled,
|
|
14
|
+
onScroll,
|
|
15
|
+
rows,
|
|
16
|
+
wrap,
|
|
17
|
+
"aria-describedby": ariaDescribedBy,
|
|
18
|
+
"aria-invalid": ariaInvalid,
|
|
19
|
+
"aria-label": ariaLabel,
|
|
20
|
+
...props
|
|
21
|
+
}: CodeEditorProps) {
|
|
22
|
+
const lines = splitLines(value);
|
|
23
|
+
const isReadonly = readonly ?? readOnly ?? false;
|
|
24
|
+
const isInvalid =
|
|
25
|
+
ariaInvalid === true ||
|
|
26
|
+
ariaInvalid === "true" ||
|
|
27
|
+
ariaInvalid === "grammar" ||
|
|
28
|
+
ariaInvalid === "spelling";
|
|
29
|
+
const languageLabel = language || "text";
|
|
30
|
+
const lineCount = lines.length;
|
|
31
|
+
const visibleRows = rows ?? Math.min(Math.max(lineCount, 8), 16);
|
|
32
|
+
const statusId = React.useId();
|
|
33
|
+
const gutterRef = React.useRef<HTMLDivElement>(null);
|
|
34
|
+
const describedBy = [ariaDescribedBy, statusId].filter(Boolean).join(" ") || undefined;
|
|
35
|
+
const modeLabel = disabled ? "Disabled" : isReadonly ? "Read-only" : "Editable";
|
|
36
|
+
|
|
37
|
+
const handleScroll = (event: React.UIEvent<HTMLTextAreaElement>) => {
|
|
38
|
+
if (gutterRef.current) {
|
|
39
|
+
gutterRef.current.scrollTop = event.currentTarget.scrollTop;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
onScroll?.(event);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<section
|
|
47
|
+
className={`eth-dev-code-editor ${className ?? ""}`}
|
|
48
|
+
data-eth-component="CodeEditor"
|
|
49
|
+
data-language={language}
|
|
50
|
+
data-readonly={isReadonly ? "true" : undefined}
|
|
51
|
+
data-disabled={disabled ? "true" : undefined}
|
|
52
|
+
data-invalid={isInvalid ? "true" : undefined}
|
|
53
|
+
data-schema-ref={schemaRef}
|
|
54
|
+
>
|
|
55
|
+
<header className="eth-dev-code-editor__header">
|
|
56
|
+
<div className="eth-dev-code-editor__heading">
|
|
57
|
+
<span className="eth-dev-code-editor__title">Code editor</span>
|
|
58
|
+
<span className="eth-dev-code-editor__language">{languageLabel}</span>
|
|
59
|
+
</div>
|
|
60
|
+
<div className="eth-dev-code-editor__badges" aria-hidden="true">
|
|
61
|
+
<span className="eth-dev-code-editor__badge">{modeLabel}</span>
|
|
62
|
+
{schemaRef ? <span className="eth-dev-code-editor__badge">{schemaRef}</span> : null}
|
|
63
|
+
</div>
|
|
64
|
+
</header>
|
|
65
|
+
<div className="eth-dev-code-editor__body">
|
|
66
|
+
<div ref={gutterRef} className="eth-dev-code-editor__gutter" aria-hidden="true">
|
|
67
|
+
{lines.map((_, index) => (
|
|
68
|
+
<span key={index}>{index + 1}</span>
|
|
69
|
+
))}
|
|
70
|
+
</div>
|
|
71
|
+
<textarea
|
|
72
|
+
{...props}
|
|
73
|
+
aria-describedby={describedBy}
|
|
74
|
+
aria-invalid={ariaInvalid}
|
|
75
|
+
aria-label={ariaLabel ?? `${languageLabel} code editor`}
|
|
76
|
+
value={value}
|
|
77
|
+
disabled={disabled}
|
|
78
|
+
readOnly={isReadonly}
|
|
79
|
+
rows={visibleRows}
|
|
80
|
+
spellCheck={false}
|
|
81
|
+
wrap={wrap ?? "off"}
|
|
82
|
+
className="eth-dev-code-editor__textarea"
|
|
83
|
+
onChange={(event) => onChange?.(event.currentTarget.value)}
|
|
84
|
+
onScroll={handleScroll}
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
<footer id={statusId} className="eth-dev-code-editor__footer">
|
|
88
|
+
<span>{modeLabel}</span>
|
|
89
|
+
<span>
|
|
90
|
+
{lineCount} {lineCount === 1 ? "line" : "lines"} / {value.length} chars
|
|
91
|
+
</span>
|
|
92
|
+
</footer>
|
|
93
|
+
</section>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { EmptyState } from "@echothink-ui/core";
|
|
3
|
+
import { DataTable, type DataColumn } from "@echothink-ui/data";
|
|
4
|
+
import type { CommitListProps, CommitRow } from "./types";
|
|
5
|
+
import { compactSha } from "./devUtils";
|
|
6
|
+
|
|
7
|
+
function authorInitials(author: string) {
|
|
8
|
+
const parts = author.trim().split(/\s+/).filter(Boolean);
|
|
9
|
+
if (!parts.length) return "?";
|
|
10
|
+
if (parts.length === 1) return parts[0].slice(0, 2).toUpperCase();
|
|
11
|
+
return `${parts[0][0]}${parts[parts.length - 1][0]}`.toUpperCase();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function CommitList({ commits, className, ...props }: CommitListProps) {
|
|
15
|
+
const latestCommit = commits[0];
|
|
16
|
+
const authorCount = new Set(commits.map((commit) => commit.author)).size;
|
|
17
|
+
const columns: DataColumn<CommitRow>[] = [
|
|
18
|
+
{
|
|
19
|
+
key: "sha",
|
|
20
|
+
header: "SHA",
|
|
21
|
+
width: "8.5rem",
|
|
22
|
+
render: (row) => (
|
|
23
|
+
<code className="eth-dev-commit-list__sha" title={row.sha}>
|
|
24
|
+
{compactSha(row.sha)}
|
|
25
|
+
</code>
|
|
26
|
+
)
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
key: "message",
|
|
30
|
+
header: "Message",
|
|
31
|
+
render: (row) => (
|
|
32
|
+
<span className="eth-dev-commit-list__message" title={row.message}>
|
|
33
|
+
{row.message}
|
|
34
|
+
</span>
|
|
35
|
+
)
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: "author",
|
|
39
|
+
header: "Author",
|
|
40
|
+
width: "12rem",
|
|
41
|
+
render: (row) => (
|
|
42
|
+
<span className="eth-dev-commit-list__author">
|
|
43
|
+
<span className="eth-dev-commit-list__avatar" aria-hidden="true">
|
|
44
|
+
{authorInitials(row.author)}
|
|
45
|
+
</span>
|
|
46
|
+
<span className="eth-dev-commit-list__author-name">{row.author}</span>
|
|
47
|
+
</span>
|
|
48
|
+
)
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
key: "date",
|
|
52
|
+
header: "Date",
|
|
53
|
+
width: "10rem",
|
|
54
|
+
render: (row) => <span className="eth-dev-commit-list__date">{row.date}</span>
|
|
55
|
+
}
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<section
|
|
60
|
+
{...props}
|
|
61
|
+
aria-label={props["aria-label"] ?? "Commit history"}
|
|
62
|
+
className={`eth-dev-commit-list ${className ?? ""}`}
|
|
63
|
+
data-eth-component="CommitList"
|
|
64
|
+
>
|
|
65
|
+
<header className="eth-dev-commit-list__header">
|
|
66
|
+
<div className="eth-dev-commit-list__heading">
|
|
67
|
+
<p>Source control</p>
|
|
68
|
+
<h3>Commit history</h3>
|
|
69
|
+
</div>
|
|
70
|
+
<dl className="eth-dev-commit-list__summary" aria-label="Commit history summary">
|
|
71
|
+
<div>
|
|
72
|
+
<dt>Commits</dt>
|
|
73
|
+
<dd>{commits.length}</dd>
|
|
74
|
+
</div>
|
|
75
|
+
<div>
|
|
76
|
+
<dt>Authors</dt>
|
|
77
|
+
<dd>{authorCount}</dd>
|
|
78
|
+
</div>
|
|
79
|
+
<div>
|
|
80
|
+
<dt>Latest</dt>
|
|
81
|
+
<dd>{latestCommit ? compactSha(latestCommit.sha) : "None"}</dd>
|
|
82
|
+
</div>
|
|
83
|
+
</dl>
|
|
84
|
+
</header>
|
|
85
|
+
<DataTable
|
|
86
|
+
rows={commits}
|
|
87
|
+
columns={columns}
|
|
88
|
+
rowKey="sha"
|
|
89
|
+
density="compact"
|
|
90
|
+
className="eth-dev-commit-list__table"
|
|
91
|
+
emptyState={
|
|
92
|
+
<EmptyState
|
|
93
|
+
title="No commits"
|
|
94
|
+
description="Commit history will appear when this branch receives changes."
|
|
95
|
+
/>
|
|
96
|
+
}
|
|
97
|
+
/>
|
|
98
|
+
</section>
|
|
99
|
+
);
|
|
100
|
+
}
|