Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
d684b89
feat: FIT-976: [FSM UI] Data Manager State Column
yyassi-heartex Nov 12, 2025
8ecd7f1
Merge remote-tracking branch 'origin/develop' into fb-fit-976/state-c…
yyassi-heartex Nov 12, 2025
5810467
Merge remote-tracking branch 'origin/develop' into fb-fit-976/state-c…
yyassi-heartex Nov 12, 2025
fcbe07b
feat: FIT-983: [FSM UI] StateChip popover with history
yyassi-heartex Nov 12, 2025
eccb395
improved state filter visuals
yyassi-heartex Nov 13, 2025
675939d
Merge branch 'fb-fit-976/state-column' into fb-fit-983/state-history-…
yyassi-heartex Nov 13, 2025
689f4cd
switching to a different endpoint
yyassi-heartex Nov 13, 2025
5e217c1
Merge remote-tracking branch 'origin/develop' into fb-fit-983/state-h…
yyassi-heartex Nov 13, 2025
cdd81b4
using api provider and unifying useQuery hook
yyassi-heartex Nov 13, 2025
21be4e4
refactoring state-chip
yyassi-heartex Nov 13, 2025
a6c521c
Merge remote-tracking branch 'origin/develop' into fb-fit-983/state-h…
yyassi-heartex Nov 14, 2025
3b08dc1
we have full e2e popover
yyassi-heartex Nov 14, 2025
c907553
lint
yyassi-heartex Nov 15, 2025
01ef97d
cleaning up transition name
yyassi-heartex Nov 15, 2025
74f3585
Merge remote-tracking branch 'origin/develop' into fb-fit-983/state-h…
yyassi-heartex Nov 17, 2025
55f05d7
linting + switching to use our Button component
yyassi-heartex Nov 17, 2025
465eb94
refactoring to move everything into app-common
yyassi-heartex Nov 17, 2025
bee7826
cleaning up css usage
yyassi-heartex Nov 17, 2025
c9dc34f
lint
yyassi-heartex Nov 17, 2025
e330f27
Merge remote-tracking branch 'origin/develop' into fb-fit-983/state-h…
yyassi-heartex Nov 18, 2025
1f27d5c
now using typography and replacing tailwind classes
yyassi-heartex Nov 18, 2025
93fca22
Update web/libs/ui/src/lib/state-chip/state-chip.tsx
yyassi-heartex Nov 18, 2025
7672190
Update web/libs/ui/src/lib/state-chip/state-chip.tsx
yyassi-heartex Nov 18, 2025
063462d
Update web/libs/ui/src/lib/state-chip/state-chip.tsx
yyassi-heartex Nov 18, 2025
b315558
Update web/libs/app-common/src/components/state-chips/project-state-c…
yyassi-heartex Nov 18, 2025
753885b
Update web/libs/app-common/src/components/state-chips/task-state-chip…
yyassi-heartex Nov 18, 2025
e53f72d
moved defintions to state registry
yyassi-heartex Nov 18, 2025
52eb1f6
tweaking the filters
yyassi-heartex Nov 18, 2025
9028e15
Merge branch 'develop' into 'fb-fit-983/state-history-popover'
yyassi-heartex Nov 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions web/apps/labelstudio/src/config/ApiConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ export const API_CONFIG = {

accessTokenSettings: "GET:/jwt/settings",
accessTokenUpdateSettings: "POST:/jwt/settings",

// FSM
fsmStateHistory: "GET:/fsm/entities/:entityType/:entityId/history",
},
alwaysExpectJSON: false,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* AnnotationStateChip - Annotation-specific state chip with history popover
*/

import { useState } from "react";
import { StateChip } from "@humansignal/ui";
import { getStateColorClass, formatStateName, getStateDescription } from "./utils";
import { StateHistoryPopoverContent } from "./state-history-popover-content";

export interface AnnotationStateChipProps {
/**
* Current state of the annotation
*/
state: string;

/**
* Annotation ID for fetching state history
*/
annotationId?: number;

/**
* Whether the chip should be interactive (show history popover)
*/
interactive?: boolean;
}

export function AnnotationStateChip({ state, annotationId, interactive = true }: AnnotationStateChipProps) {
const [open, setOpen] = useState(false);

const label = formatStateName(state);
const description = getStateDescription(state, "annotation");
const colorClasses = getStateColorClass(state);

const popoverContent = annotationId ? (
<StateHistoryPopoverContent entityType="annotation" entityId={annotationId} isOpen={open} />
) : undefined;

return (
<StateChip
label={label}
description={description}
className={colorClasses}
interactive={interactive && !!annotationId}
popoverContent={popoverContent}
open={open}
onOpenChange={setOpen}
/>
);
}
7 changes: 7 additions & 0 deletions web/libs/app-common/src/components/state-chips/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export { TaskStateChip, type TaskStateChipProps } from "./task-state-chip";
export { AnnotationStateChip, type AnnotationStateChipProps } from "./annotation-state-chip";
export { ProjectStateChip, type ProjectStateChipProps } from "./project-state-chip";
export { StateHistoryPopoverContent, type StateHistoryPopoverContentProps } from "./state-history-popover-content";
export { StateHistoryPopover, type StateHistoryPopoverProps } from "./state-history-popover";
export * from "./utils";
export { stateRegistry, StateType, type EntityType, type StateMetadata } from "./state-registry";
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/**
* ProjectStateChip - Project-specific state chip with history popover
*/

import { useState } from "react";
import { StateChip } from "@humansignal/ui";
import { getStateColorClass, formatStateName, getStateDescription } from "./utils";
import { StateHistoryPopoverContent } from "./state-history-popover-content";

export interface ProjectStateChipProps {
/**
* Current state of the project
*/
state: string;

/**
* Project ID for fetching state history
*/
projectId?: number;

/**
* Whether the chip should be interactive (show history popover)
*/
interactive?: boolean;
}

export function ProjectStateChip({ state, projectId, interactive = true }: ProjectStateChipProps) {
const [open, setOpen] = useState(false);

const label = formatStateName(state);
const description = getStateDescription(state, "project");
const colorClasses = getStateColorClass(state);

const popoverContent = projectId ? (
<StateHistoryPopoverContent entityType="project" entityId={projectId} isOpen={open} />
) : undefined;

return (
<StateChip
label={label}
description={description}
className={colorClasses}
interactive={interactive && !!projectId}
popoverContent={popoverContent}
open={open}
onOpenChange={setOpen}
/>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* StateHistoryPopoverContent - Popover content for displaying state history
*/

import { Badge, Button, Typography } from "@humansignal/ui";
import { IconSync, IconError, IconHistoryRewind } from "@humansignal/icons";
import { useStateHistory, type StateHistoryItem } from "../../hooks/useStateHistory";
import { getStateColorClass, formatStateName, formatTimestamp, formatUserName } from "./utils";

export interface StateHistoryPopoverContentProps {
entityType: "task" | "annotation" | "project";
entityId: number;
isOpen: boolean;
}

export function StateHistoryPopoverContent({ entityType, entityId, isOpen }: StateHistoryPopoverContentProps) {
const { data, isLoading, isError, error, refetch } = useStateHistory({
entityType,
entityId,
enabled: isOpen,
});

const history = (data?.results || []) as StateHistoryItem[];

return (
<div
className="flex flex-col w-[320px] max-h-[400px] bg-primary-background rounded-lg shadow-lg"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="px-4 py-3 border-b border-neutral-border">
<div className="flex items-center gap-2">
<IconHistoryRewind className="w-4 h-4 text-muted-foreground" />
<Typography variant="body" size="small" className="font-medium text-neutral-foreground">
State History
</Typography>
</div>
</div>

{/* Content */}
<div className="flex-1 overflow-y-auto p-4">
{isLoading && (
<div className="flex flex-col items-center justify-center py-8 gap-3">
<IconSync className="w-8 h-8 text-primary-icon animate-spin" />
<Typography variant="body" size="small" className="text-neutral-content-subtle">
Loading...
</Typography>
</div>
)}

{isError && (
<div className="flex flex-col items-center justify-center py-8 gap-3">
<IconError className="w-8 h-8 text-negative-icon" />
<Typography variant="body" size="small" className="text-neutral-foreground">
Failed to load history
</Typography>
<Typography variant="body" size="smallest" className="text-neutral-content-subtle text-center">
{error instanceof Error ? error.message : "Unknown error"}
</Typography>
<Button
onClick={(e) => {
e.stopPropagation();
refetch();
}}
className="mt-tight"
size="smaller"
variant="primary"
type="button"
>
Retry
</Button>
</div>
)}

{!isLoading && !isError && history.length === 0 && (
<div className="flex flex-col items-center justify-center py-8 gap-3">
<IconHistoryRewind className="w-8 h-8 text-neutral-content-subtler" />
<Typography variant="body" size="small" className="text-neutral-content-subtle">
No history available
</Typography>
</div>
)}

{!isLoading && !isError && history.length > 0 && (
<div className="space-y-3">
{history.map((item: StateHistoryItem, index: number) => (
<div key={index} className="pb-3 border-b border-neutral-border last:border-0 last:pb-0">
<div className="flex items-center justify-between mb-2">
<Badge className={getStateColorClass(item.state)}>{formatStateName(item.state)}</Badge>
<Typography variant="body" size="smallest" className="text-neutral-content-subtle">
{formatTimestamp(item.created_at)}
</Typography>
</div>
<Typography variant="body" size="smallest" className="text-muted-foreground">
By: {formatUserName(item.triggered_by)}
</Typography>
</div>
))}
</div>
)}
</div>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
/**
* StateHistoryPopover component
* Displays the complete FSM state transition history for an entity
*/

import type React from "react";
import { Popover, Badge, Button, Typography } from "@humansignal/ui";
import { IconSync, IconError, IconHistoryRewind } from "@humansignal/icons";
import { useStateHistory, type StateHistoryItem } from "../../hooks/useStateHistory";
import { getStateColorClass, formatStateName, formatTimestamp, formatUserName } from "./utils";

export interface StateHistoryPopoverProps {
trigger: React.ReactNode;
entityType: "task" | "annotation" | "project";
entityId: number;
currentState: string;
open?: boolean;
onOpenChange?: (open: boolean) => void;
}

export function StateHistoryPopover({
trigger,
entityType,
entityId,
currentState,
open,
onOpenChange,
}: StateHistoryPopoverProps) {
const { data, isLoading, isError, error, refetch } = useStateHistory({
entityType,
entityId,
enabled: open ?? true,
});

const history = (data?.results || []) as StateHistoryItem[];

return (
<Popover trigger={trigger} open={open} onOpenChange={onOpenChange} align="start" sideOffset={8}>
<div
className="flex flex-col w-[320px] max-h-[400px] bg-primary-background rounded-lg shadow-lg"
onClick={(e) => e.stopPropagation()}
>
{/* Header */}
<div className="px-4 py-3 border-b border-neutral-border">
<div className="flex items-center gap-2">
<IconHistoryRewind className="w-4 h-4 " />
<Typography variant="body" size="small" className="font-medium text-neutral-foreground">
State History
</Typography>
</div>
</div>

{/* Content */}
<div className="flex-1 overflow-y-auto p-4">
{isLoading && (
<div className="flex flex-col items-center justify-center py-8 gap-3">
<IconSync className="w-8 h-8 text-primary-icon animate-spin" />
<Typography variant="body" size="small" className="text-neutral-content-subtle">
Loading...
</Typography>
</div>
)}

{isError && (
<div className="flex flex-col items-center justify-center py-8 gap-3">
<IconError className="w-8 h-8 text-negative-icon" />
<Typography variant="body" size="small" className="text-neutral-foreground">
Failed to load history
</Typography>
<Typography variant="body" size="smallest" className="text-neutral-content-subtle text-center">
{error instanceof Error ? error.message : "Unknown error"}
</Typography>
<Button
type="button"
onClick={(e) => {
e.stopPropagation();
refetch();
}}
className="mt-tight"
size="smaller"
variant="primary"
>
Retry
</Button>
</div>
)}

{!isLoading && !isError && history.length === 0 && (
<div className="flex flex-col items-center justify-center py-8 gap-3">
<IconHistoryRewind className="w-8 h-8 text-neutral-content-subtler" />
<Typography variant="body" size="small" className="text-neutral-content-subtle">
No history available
</Typography>
</div>
)}

{!isLoading && !isError && history.length > 0 && (
<div className="space-y-3">
{history.map((item: StateHistoryItem, index: number) => (
<div key={index} className="pb-3 border-b border-neutral-border last:border-0 last:pb-0">
<div className="flex items-center justify-between mb-2">
<Badge className={getStateColorClass(item.state)}>{formatStateName(item.state)}</Badge>
<Typography variant="body" size="smallest" className="text-neutral-content-subtle">
{formatTimestamp(item.created_at)}
</Typography>
</div>
<div>
<Typography variant="body" size="smallest" className="text-muted-foreground">
By: {formatUserName(item.triggered_by)}
</Typography>
{item.transition_name && (
<Typography variant="body" size="smallest" className="mt-1 text-neutral-content-subtle">
{item.transition_name}
</Typography>
)}
</div>
</div>
))}
</div>
)}
</div>
</div>
</Popover>
);
}
Loading
Loading