325 lines
7.7 KiB
TypeScript
325 lines
7.7 KiB
TypeScript
|
|
import type { ReviewStory } from "../api/workflow";
|
||
|
|
|
||
|
|
interface ReviewPanelProps {
|
||
|
|
reviewQueue: ReviewStory[];
|
||
|
|
isReviewLoading: boolean;
|
||
|
|
reviewError: string | null;
|
||
|
|
proceedingStoryId: string | null;
|
||
|
|
storyId: string;
|
||
|
|
isGateLoading: boolean;
|
||
|
|
proceedError: string | null;
|
||
|
|
proceedSuccess: string | null;
|
||
|
|
lastReviewRefresh: Date | null;
|
||
|
|
onRefresh: () => void;
|
||
|
|
onProceed: (storyId: string) => Promise<void>;
|
||
|
|
}
|
||
|
|
|
||
|
|
const formatTimestamp = (value: Date | null): string => {
|
||
|
|
if (!value) return "—";
|
||
|
|
return value.toLocaleTimeString([], {
|
||
|
|
hour: "2-digit",
|
||
|
|
minute: "2-digit",
|
||
|
|
second: "2-digit",
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
export function ReviewPanel({
|
||
|
|
reviewQueue,
|
||
|
|
isReviewLoading,
|
||
|
|
reviewError,
|
||
|
|
proceedingStoryId,
|
||
|
|
storyId,
|
||
|
|
isGateLoading,
|
||
|
|
proceedError,
|
||
|
|
proceedSuccess,
|
||
|
|
lastReviewRefresh,
|
||
|
|
onRefresh,
|
||
|
|
onProceed,
|
||
|
|
}: ReviewPanelProps) {
|
||
|
|
return (
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
border: "1px solid #333",
|
||
|
|
borderRadius: "10px",
|
||
|
|
padding: "12px 16px",
|
||
|
|
background: "#1f1f1f",
|
||
|
|
display: "flex",
|
||
|
|
flexDirection: "column",
|
||
|
|
gap: "8px",
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
display: "flex",
|
||
|
|
alignItems: "center",
|
||
|
|
justifyContent: "space-between",
|
||
|
|
gap: "12px",
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
display: "flex",
|
||
|
|
alignItems: "center",
|
||
|
|
gap: "8px",
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<div style={{ fontWeight: 600 }}>Stories Awaiting Review</div>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={onRefresh}
|
||
|
|
disabled={isReviewLoading}
|
||
|
|
style={{
|
||
|
|
padding: "4px 10px",
|
||
|
|
borderRadius: "999px",
|
||
|
|
border: "1px solid #333",
|
||
|
|
background: isReviewLoading ? "#2a2a2a" : "#2f2f2f",
|
||
|
|
color: isReviewLoading ? "#777" : "#aaa",
|
||
|
|
cursor: isReviewLoading ? "not-allowed" : "pointer",
|
||
|
|
fontSize: "0.75em",
|
||
|
|
fontWeight: 600,
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
Refresh
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
display: "flex",
|
||
|
|
flexDirection: "column",
|
||
|
|
alignItems: "flex-end",
|
||
|
|
gap: "2px",
|
||
|
|
fontSize: "0.85em",
|
||
|
|
color: "#aaa",
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<div>
|
||
|
|
{reviewQueue.filter((story) => story.can_accept).length} ready /{" "}
|
||
|
|
{reviewQueue.length} total
|
||
|
|
</div>
|
||
|
|
<div style={{ fontSize: "0.8em", color: "#777" }}>
|
||
|
|
Updated {formatTimestamp(lastReviewRefresh)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{isReviewLoading ? (
|
||
|
|
<div style={{ fontSize: "0.85em", color: "#aaa" }}>
|
||
|
|
Loading review queue...
|
||
|
|
</div>
|
||
|
|
) : reviewError ? (
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
fontSize: "0.85em",
|
||
|
|
color: "#ff7b72",
|
||
|
|
display: "flex",
|
||
|
|
alignItems: "center",
|
||
|
|
gap: "8px",
|
||
|
|
flexWrap: "wrap",
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<span>{reviewError} Use Refresh to try again.</span>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={onRefresh}
|
||
|
|
disabled={isReviewLoading}
|
||
|
|
style={{
|
||
|
|
padding: "4px 10px",
|
||
|
|
borderRadius: "999px",
|
||
|
|
border: "1px solid #333",
|
||
|
|
background: isReviewLoading ? "#2a2a2a" : "#2f2f2f",
|
||
|
|
color: isReviewLoading ? "#777" : "#aaa",
|
||
|
|
cursor: isReviewLoading ? "not-allowed" : "pointer",
|
||
|
|
fontSize: "0.75em",
|
||
|
|
fontWeight: 600,
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
Retry
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
) : reviewQueue.length === 0 ? (
|
||
|
|
<div style={{ fontSize: "0.85em", color: "#aaa" }}>
|
||
|
|
No stories waiting for review.
|
||
|
|
</div>
|
||
|
|
) : (
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
display: "flex",
|
||
|
|
flexDirection: "column",
|
||
|
|
gap: "8px",
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{reviewQueue.map((story) => (
|
||
|
|
<div
|
||
|
|
key={`review-${story.story_id}`}
|
||
|
|
style={{
|
||
|
|
border: "1px solid #2a2a2a",
|
||
|
|
borderRadius: "8px",
|
||
|
|
padding: "10px 12px",
|
||
|
|
background: "#191919",
|
||
|
|
display: "flex",
|
||
|
|
flexDirection: "column",
|
||
|
|
gap: "6px",
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
display: "flex",
|
||
|
|
alignItems: "center",
|
||
|
|
justifyContent: "space-between",
|
||
|
|
gap: "12px",
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<div
|
||
|
|
style={{
|
||
|
|
display: "flex",
|
||
|
|
alignItems: "center",
|
||
|
|
gap: "8px",
|
||
|
|
flexWrap: "wrap",
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
<div style={{ fontWeight: 600 }}>{story.story_id}</div>
|
||
|
|
<span
|
||
|
|
style={{
|
||
|
|
padding: "2px 8px",
|
||
|
|
borderRadius: "999px",
|
||
|
|
fontSize: "0.7em",
|
||
|
|
fontWeight: 600,
|
||
|
|
background: story.can_accept ? "#7ee787" : "#ff7b72",
|
||
|
|
color: story.can_accept ? "#000" : "#1a1a1a",
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{story.can_accept ? "Ready" : "Blocked"}
|
||
|
|
</span>
|
||
|
|
{story.summary.failed > 0 && (
|
||
|
|
<span
|
||
|
|
style={{
|
||
|
|
padding: "2px 8px",
|
||
|
|
borderRadius: "999px",
|
||
|
|
fontSize: "0.7em",
|
||
|
|
fontWeight: 600,
|
||
|
|
background: "#ffb86c",
|
||
|
|
color: "#1a1a1a",
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
Failing {story.summary.failed}
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
{story.warning && (
|
||
|
|
<span
|
||
|
|
style={{
|
||
|
|
padding: "2px 8px",
|
||
|
|
borderRadius: "999px",
|
||
|
|
fontSize: "0.7em",
|
||
|
|
fontWeight: 600,
|
||
|
|
background: "#ffb86c",
|
||
|
|
color: "#1a1a1a",
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
Warning
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
{story.missing_categories.length > 0 && (
|
||
|
|
<span
|
||
|
|
style={{
|
||
|
|
padding: "2px 8px",
|
||
|
|
borderRadius: "999px",
|
||
|
|
fontSize: "0.7em",
|
||
|
|
fontWeight: 600,
|
||
|
|
background: "#3a2a1a",
|
||
|
|
color: "#ffb86c",
|
||
|
|
border: "1px solid #5a3a1a",
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
Missing
|
||
|
|
</span>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
disabled={
|
||
|
|
proceedingStoryId === story.story_id ||
|
||
|
|
isReviewLoading ||
|
||
|
|
(story.story_id === storyId && isGateLoading) ||
|
||
|
|
!story.can_accept
|
||
|
|
}
|
||
|
|
onClick={() => onProceed(story.story_id)}
|
||
|
|
style={{
|
||
|
|
padding: "6px 12px",
|
||
|
|
borderRadius: "8px",
|
||
|
|
border: "none",
|
||
|
|
background:
|
||
|
|
proceedingStoryId === story.story_id
|
||
|
|
? "#444"
|
||
|
|
: story.can_accept
|
||
|
|
? "#7ee787"
|
||
|
|
: "#333",
|
||
|
|
color:
|
||
|
|
proceedingStoryId === story.story_id
|
||
|
|
? "#bbb"
|
||
|
|
: story.can_accept
|
||
|
|
? "#000"
|
||
|
|
: "#aaa",
|
||
|
|
cursor:
|
||
|
|
proceedingStoryId === story.story_id || !story.can_accept
|
||
|
|
? "not-allowed"
|
||
|
|
: "pointer",
|
||
|
|
fontSize: "0.85em",
|
||
|
|
fontWeight: 600,
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{proceedingStoryId === story.story_id
|
||
|
|
? "Proceeding..."
|
||
|
|
: story.can_accept
|
||
|
|
? "Proceed"
|
||
|
|
: "Blocked"}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
<div style={{ fontSize: "0.85em", color: "#aaa" }}>
|
||
|
|
Summary: {story.summary.passed}/{story.summary.total} passing,{" "}
|
||
|
|
{` ${story.summary.failed}`} failing
|
||
|
|
</div>
|
||
|
|
{story.missing_categories.length > 0 && (
|
||
|
|
<div style={{ fontSize: "0.85em", color: "#ffb86c" }}>
|
||
|
|
Missing: {story.missing_categories.join(", ")}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{story.reasons.length > 0 && (
|
||
|
|
<ul
|
||
|
|
style={{
|
||
|
|
margin: "0 0 0 16px",
|
||
|
|
padding: 0,
|
||
|
|
fontSize: "0.85em",
|
||
|
|
color: "#ccc",
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{story.reasons.map((reason) => (
|
||
|
|
<li key={`review-reason-${story.story_id}-${reason}`}>
|
||
|
|
{reason}
|
||
|
|
</li>
|
||
|
|
))}
|
||
|
|
</ul>
|
||
|
|
)}
|
||
|
|
{story.warning && (
|
||
|
|
<div style={{ fontSize: "0.85em", color: "#ffb86c" }}>
|
||
|
|
{story.warning}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{proceedError && (
|
||
|
|
<div style={{ fontSize: "0.85em", color: "#ff7b72" }}>
|
||
|
|
{proceedError}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
{proceedSuccess && (
|
||
|
|
<div style={{ fontSize: "0.85em", color: "#7ee787" }}>
|
||
|
|
{proceedSuccess}
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|