@echothink-ui/quality 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 +3 -0
- package/dist/components/QCDecisionBadge.d.ts +6 -0
- package/dist/components/QCJobMonitor.d.ts +14 -0
- package/dist/components/QCMetricsInspector.d.ts +21 -0
- package/dist/components/QCProfileEditor.d.ts +18 -0
- package/dist/components/QCRoutingTimeline.d.ts +11 -0
- package/dist/index.cjs +371 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +329 -0
- package/dist/index.js.map +1 -0
- package/package.json +42 -0
- package/src/components/QCDecisionBadge.tsx +34 -0
- package/src/components/QCJobMonitor.tsx +77 -0
- package/src/components/QCMetricsInspector.tsx +108 -0
- package/src/components/QCProfileEditor.tsx +155 -0
- package/src/components/QCRoutingTimeline.tsx +35 -0
- package/src/index.tsx +14 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { ConfigForm, RuleBuilder } from "@echothink-ui/forms";
|
|
4
|
+
import type { FormValues, RuleDefinition } from "@echothink-ui/forms";
|
|
5
|
+
|
|
6
|
+
export interface QCThreshold {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
operator: string;
|
|
10
|
+
value: number | string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface QCProfile {
|
|
14
|
+
id: string;
|
|
15
|
+
language: string;
|
|
16
|
+
thresholds: QCThreshold[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface QCProfileEditorProps {
|
|
20
|
+
profile: QCProfile;
|
|
21
|
+
onChange?: (profile: QCProfile) => void;
|
|
22
|
+
onSubmit?: (profile: QCProfile) => void;
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const operatorOptions = [
|
|
27
|
+
{ value: "lt", label: "Less than" },
|
|
28
|
+
{ value: "lte", label: "Less than or equal" },
|
|
29
|
+
{ value: "gt", label: "Greater than" },
|
|
30
|
+
{ value: "gte", label: "Greater than or equal" },
|
|
31
|
+
{ value: "eq", label: "Equals" },
|
|
32
|
+
{ value: "between", label: "Between" }
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
export function QCProfileEditor({ profile, onChange, onSubmit, className }: QCProfileEditorProps) {
|
|
36
|
+
const values = React.useMemo(() => valuesFromProfile(profile), [profile]);
|
|
37
|
+
const rules = React.useMemo(() => rulesFromProfile(profile), [profile]);
|
|
38
|
+
|
|
39
|
+
return (
|
|
40
|
+
<div className={clsx("eth-quality-qc-profile-editor", className)} data-eth-component="QCProfileEditor">
|
|
41
|
+
<ConfigForm
|
|
42
|
+
title="QC profile"
|
|
43
|
+
description={`Profile ${profile.id}`}
|
|
44
|
+
sections={[
|
|
45
|
+
{
|
|
46
|
+
id: "profile",
|
|
47
|
+
title: "Thresholds",
|
|
48
|
+
description: "Language and threshold values used by the automated QC job.",
|
|
49
|
+
fields: [
|
|
50
|
+
{
|
|
51
|
+
name: "language",
|
|
52
|
+
type: "text",
|
|
53
|
+
label: "Language",
|
|
54
|
+
required: true
|
|
55
|
+
},
|
|
56
|
+
...profile.thresholds.flatMap((threshold) => [
|
|
57
|
+
{
|
|
58
|
+
name: operatorKey(threshold.id),
|
|
59
|
+
type: "select" as const,
|
|
60
|
+
label: `${threshold.label} operator`,
|
|
61
|
+
options: operatorOptions
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: valueKey(threshold.id),
|
|
65
|
+
type: typeof threshold.value === "number" ? ("number" as const) : ("text" as const),
|
|
66
|
+
label: `${threshold.label} value`
|
|
67
|
+
}
|
|
68
|
+
])
|
|
69
|
+
]
|
|
70
|
+
}
|
|
71
|
+
]}
|
|
72
|
+
values={values}
|
|
73
|
+
submitLabel="Save profile"
|
|
74
|
+
onChange={(nextValues) => onChange?.(profileFromValues(profile, nextValues))}
|
|
75
|
+
onSubmit={(nextValues) => onSubmit?.(profileFromValues(profile, nextValues))}
|
|
76
|
+
/>
|
|
77
|
+
<RuleBuilder
|
|
78
|
+
className="eth-quality-qc-profile-editor__rules"
|
|
79
|
+
rules={rules}
|
|
80
|
+
variables={profile.thresholds.map((threshold) => threshold.id)}
|
|
81
|
+
onChange={(nextRules) => onChange?.(profileFromRules(profile, nextRules))}
|
|
82
|
+
/>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function valuesFromProfile(profile: QCProfile): FormValues {
|
|
88
|
+
return profile.thresholds.reduce<FormValues>(
|
|
89
|
+
(values, threshold) => ({
|
|
90
|
+
...values,
|
|
91
|
+
[operatorKey(threshold.id)]: threshold.operator,
|
|
92
|
+
[valueKey(threshold.id)]: threshold.value
|
|
93
|
+
}),
|
|
94
|
+
{ language: profile.language }
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function profileFromValues(profile: QCProfile, values: FormValues): QCProfile {
|
|
99
|
+
return {
|
|
100
|
+
...profile,
|
|
101
|
+
language: String(values.language ?? ""),
|
|
102
|
+
thresholds: profile.thresholds.map((threshold) => ({
|
|
103
|
+
...threshold,
|
|
104
|
+
operator: String(values[operatorKey(threshold.id)] ?? threshold.operator),
|
|
105
|
+
value: normalizeThresholdValue(threshold.value, values[valueKey(threshold.id)])
|
|
106
|
+
}))
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function rulesFromProfile(profile: QCProfile): RuleDefinition[] {
|
|
111
|
+
return profile.thresholds.map((threshold) => ({
|
|
112
|
+
id: threshold.id,
|
|
113
|
+
when: {
|
|
114
|
+
kind: "leaf",
|
|
115
|
+
field: threshold.id,
|
|
116
|
+
op: threshold.operator,
|
|
117
|
+
value: threshold.value
|
|
118
|
+
},
|
|
119
|
+
then: {
|
|
120
|
+
type: "require-approval",
|
|
121
|
+
target: "manual-review"
|
|
122
|
+
}
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function profileFromRules(profile: QCProfile, rules: RuleDefinition[]): QCProfile {
|
|
127
|
+
return {
|
|
128
|
+
...profile,
|
|
129
|
+
thresholds: profile.thresholds.map((threshold) => {
|
|
130
|
+
const rule = rules.find((item) => item.id === threshold.id);
|
|
131
|
+
if (!rule) return threshold;
|
|
132
|
+
return {
|
|
133
|
+
...threshold,
|
|
134
|
+
operator: rule.when.op ?? threshold.operator,
|
|
135
|
+
value: normalizeThresholdValue(threshold.value, rule.when.value)
|
|
136
|
+
};
|
|
137
|
+
})
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function normalizeThresholdValue(previousValue: string | number, nextValue: unknown) {
|
|
142
|
+
if (typeof previousValue === "number") {
|
|
143
|
+
const numeric = Number(nextValue);
|
|
144
|
+
return Number.isNaN(numeric) ? previousValue : numeric;
|
|
145
|
+
}
|
|
146
|
+
return String(nextValue ?? "");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function operatorKey(id: string) {
|
|
150
|
+
return `${id}.operator`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function valueKey(id: string) {
|
|
154
|
+
return `${id}.value`;
|
|
155
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import clsx from "clsx";
|
|
3
|
+
import { Badge, Surface } from "@echothink-ui/core";
|
|
4
|
+
import type { EthSeverity } from "@echothink-ui/core";
|
|
5
|
+
|
|
6
|
+
export interface QCRoutingEvent {
|
|
7
|
+
at: string;
|
|
8
|
+
label: string;
|
|
9
|
+
severity?: EthSeverity;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface QCRoutingTimelineProps {
|
|
13
|
+
events: QCRoutingEvent[];
|
|
14
|
+
className?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function QCRoutingTimeline({ events, className }: QCRoutingTimelineProps) {
|
|
18
|
+
return (
|
|
19
|
+
<Surface
|
|
20
|
+
className={clsx("eth-quality-qc-routing-timeline", className)}
|
|
21
|
+
title="Routing timeline"
|
|
22
|
+
data-eth-component="QCRoutingTimeline"
|
|
23
|
+
>
|
|
24
|
+
<ol className="eth-quality-qc-routing-timeline__events">
|
|
25
|
+
{events.map((event) => (
|
|
26
|
+
<li key={`${event.at}-${event.label}`}>
|
|
27
|
+
<time>{event.at}</time>
|
|
28
|
+
<Badge severity={event.severity ?? "info"}>{event.severity ?? "info"}</Badge>
|
|
29
|
+
<span>{event.label}</span>
|
|
30
|
+
</li>
|
|
31
|
+
))}
|
|
32
|
+
</ol>
|
|
33
|
+
</Surface>
|
|
34
|
+
);
|
|
35
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export * from "./components/QCMetricsInspector";
|
|
2
|
+
export * from "./components/QCProfileEditor";
|
|
3
|
+
export * from "./components/QCJobMonitor";
|
|
4
|
+
export * from "./components/QCDecisionBadge";
|
|
5
|
+
export * from "./components/QCRoutingTimeline";
|
|
6
|
+
|
|
7
|
+
export const QualityComponentNames = [
|
|
8
|
+
"QCMetricsInspector",
|
|
9
|
+
"QCProfileEditor",
|
|
10
|
+
"QCJobMonitor",
|
|
11
|
+
"QCDecisionBadge",
|
|
12
|
+
"QCRoutingTimeline"
|
|
13
|
+
] as const;
|
|
14
|
+
export type QualityComponentName = (typeof QualityComponentNames)[number];
|