Skip to content

Commit

Permalink
✨ Show worker status in document list
Browse files Browse the repository at this point in the history
  • Loading branch information
pajowu committed Aug 2, 2023
1 parent bf1ef14 commit 3c46ae4
Show file tree
Hide file tree
Showing 7 changed files with 144 additions and 27 deletions.
35 changes: 34 additions & 1 deletion backend/openapi-schema.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,39 @@ components:
- document_id
title: AlignTask
type: object
ApiDocumentWithTasks:
properties:
changed_at:
title: Changed At
type: string
created_at:
title: Created At
type: string
id:
title: Id
type: string
media_files:
items:
$ref: '#/components/schemas/DocumentMedia'
title: Media Files
type: array
name:
title: Name
type: string
tasks:
items:
$ref: '#/components/schemas/TaskResponse'
title: Tasks
type: array
required:
- id
- name
- created_at
- changed_at
- media_files
- tasks
title: ApiDocumentWithTasks
type: object
AssignedTaskResponse:
properties:
current_attempt:
Expand Down Expand Up @@ -516,7 +549,7 @@ paths:
application/json:
schema:
items:
$ref: '#/components/schemas/Document'
$ref: '#/components/schemas/ApiDocumentWithTasks'
title: Response List Documents Api V1 Documents Get
type: array
description: Successful Response
Expand Down
24 changes: 19 additions & 5 deletions backend/transcribee_backend/models/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@

from transcribee_backend import media_storage

from .user import User


class DocumentBase(SQLModel):
name: str
duration: Optional[float] = None # In seconds


class ApiDocumentWithTasks(ApiDocument):
tasks: List["TaskResponse"]


class Document(DocumentBase, table=True):
id: uuid.UUID = Field(
default_factory=uuid.uuid4,
Expand All @@ -24,7 +26,7 @@ class Document(DocumentBase, table=True):
nullable=False,
)
user_id: uuid.UUID = Field(foreign_key="user.id")
user: User = Relationship()
user: "User" = Relationship()
created_at: datetime.datetime = Field(
sa_column=Column(DateTime(timezone=True), nullable=False)
)
Expand All @@ -38,16 +40,19 @@ class Document(DocumentBase, table=True):
share_tokens: List["DocumentShareToken"] = Relationship(
sa_relationship_kwargs={"cascade": "all,delete"}
)
tasks: List["Task"] = Relationship(sa_relationship_kwargs={"cascade": "all,delete"})

def as_api_document(self) -> ApiDocument:
return ApiDocument(
def as_api_document(self) -> ApiDocumentWithTasks:
tasks = [TaskResponse.from_orm(task) for task in self.tasks]
return ApiDocumentWithTasks(
id=str(self.id),
name=self.name,
created_at=self.created_at.isoformat(),
changed_at=self.created_at.isoformat(),
media_files=[
media_file.as_api_media_file() for media_file in self.media_files
],
tasks=tasks,
)


Expand Down Expand Up @@ -132,3 +137,12 @@ class DocumentShareToken(DocumentShareTokenBase, table=True):
nullable=False,
)
document: Document = Relationship(back_populates="share_tokens")


# Import here to break circular dependency

from .task import Task, TaskResponse # noqa: E402
from .user import User # noqa: E402

ApiDocumentWithTasks.update_forward_refs()
Document.update_forward_refs()
2 changes: 1 addition & 1 deletion backend/transcribee_backend/models/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class Task(TaskBase, table=True):
sa_column=Column(GUID, ForeignKey("document.id", ondelete="CASCADE")),
unique=False,
)
document: Document = Relationship()
document: Document = Relationship(back_populates="tasks")

task_parameters: dict = Field(sa_column=Column(JSON(), nullable=False))

Expand Down
7 changes: 5 additions & 2 deletions backend/transcribee_backend/routers/document.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,10 @@
from transcribee_backend.db import get_session
from transcribee_backend.helpers.sync import DocumentSyncConsumer
from transcribee_backend.helpers.time import now_tz_aware
from transcribee_backend.models.document import DocumentShareTokenBase
from transcribee_backend.models.document import (
ApiDocumentWithTasks,
DocumentShareTokenBase,
)
from transcribee_backend.models.task import TaskAttempt, TaskResponse

from .. import media_storage
Expand Down Expand Up @@ -358,7 +361,7 @@ async def create_document(
def list_documents(
session: Session = Depends(get_session),
token: UserToken = Depends(get_user_token),
) -> List[ApiDocument]:
) -> List[ApiDocumentWithTasks]:
statement = (
select(Document)
.where(Document.user == token.user)
Expand Down
12 changes: 10 additions & 2 deletions frontend/src/editor/worker_status.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ function getColor(task: Task | null, dark: boolean): string {
return (str && color_map[str]) || color_map['DEFAULT'];
}

function getWorkerStatusString(isWorking: boolean, isFailed: boolean): string {
export function getWorkerStatusString(isWorking: boolean, isFailed: boolean): string {
if (isFailed) {
return 'failed';
} else if (isWorking) {
Expand All @@ -43,9 +43,14 @@ function getWorkerStatusString(isWorking: boolean, isFailed: boolean): string {
}
}
export function WorkerStatus({ documentId }: { documentId: string }) {
const { data } = useGetDocumentTasks({ document_id: documentId }, { refreshInterval: 1 });

return <WorkerStatusWithData data={data} />;
}

export function WorkerStatusWithData({ data }: { data: Task[] }) {
const systemPrefersDark = useMediaQuery('(prefers-color-scheme: dark)');

const { data } = useGetDocumentTasks({ document_id: documentId }, { refreshInterval: 1 });
const isWorking = data?.some((task) => task.state !== 'COMPLETED');
const isFailed = data?.some((task) => task.state == 'FAILED');

Expand All @@ -69,6 +74,9 @@ export function WorkerStatus({ documentId }: { documentId: string }) {
})}
/>
}
onClick={(e) => {
e.preventDefault();
}}
>
<span className="pl-7 font-bold">Worker Tasks</span>
<svg className="py-2" height={Math.max(...(yPositionsText || [0])) + 30} width={250}>
Expand Down
17 changes: 16 additions & 1 deletion frontend/src/openapi-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,21 @@ export interface components {
*/
task_type?: "ALIGN";
};
/** ApiDocumentWithTasks */
ApiDocumentWithTasks: {
/** Changed At */
changed_at: string;
/** Created At */
created_at: string;
/** Id */
id: string;
/** Media Files */
media_files: (components["schemas"]["DocumentMedia"])[];
/** Name */
name: string;
/** Tasks */
tasks: (components["schemas"]["TaskResponse"])[];
};
/** AssignedTaskResponse */
AssignedTaskResponse: {
current_attempt?: components["schemas"]["TaskAttemptResponse"];
Expand Down Expand Up @@ -435,7 +450,7 @@ export interface operations {
/** @description Successful Response */
200: {
content: {
"application/json": (components["schemas"]["Document"])[];
"application/json": (components["schemas"]["ApiDocumentWithTasks"])[];
};
};
/** @description Validation Error */
Expand Down
74 changes: 59 additions & 15 deletions frontend/src/pages/user_home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,46 @@ import { AiOutlinePlus } from 'react-icons/ai';
import { IoMdTrash } from 'react-icons/io';
import { IconButton } from '../components/button';
import { Version } from '../common/version';
import { WorkerStatusWithData } from '../editor/worker_status';
import { useEffect, useState } from 'react';

type Tasks = ReturnType<typeof useListDocuments>['data'][0]['tasks'];

function getTaskProgress(tasks: Tasks) {
if (tasks.length == 0) return 0;
const totalTasks = tasks.length;
const completedTasks = tasks.filter((task) => task.state == 'COMPLETED').length;
const runningTasks = tasks
.filter((task) => task.state == 'ASSIGNED')
.map((task) => task.current_attempt?.progress || 0)
.reduce((a, b) => a + b, 0);
return (completedTasks + runningTasks) / totalTasks;
}

export function UserHomePage() {
const { data, mutate } = useListDocuments({});
// Trusting the SWR documentation, we *should* be able to just set `refreshInterval` to a function
// which is then called after new data is fetched to calculate the interval after which the next
// data should be fetched. Sadly, this does not work, as SWR passed the stale data without an
// indication if the data is stale :(
// https://swr.vercel.app/docs/api#options
// https://github.com/vercel/swr/blob/d1b7169adf01feaf47f46c556208770680680f6f/core/src/use-swr.ts#L643-L657
const [refreshInterval, setRefreshInterval] = useState(5000);
const { data, mutate } = useListDocuments(
{},
{
refreshInterval: refreshInterval,
refreshWhenHidden: false,
revalidateOnFocus: true,
},
);
useEffect(() => {
const hasUnfinishedDocuments = data?.some((doc) => getTaskProgress(doc.tasks) < 1);
// Refresh every second if there are still unfinished documents to update the task progress
// and every hour otherwise
const refreshInterval =
hasUnfinishedDocuments || hasUnfinishedDocuments === undefined ? 1 : 60 * 60;
setRefreshInterval(refreshInterval * 1000);
}, [data]);

return (
<AppContainer>
Expand Down Expand Up @@ -57,20 +94,27 @@ export function UserHomePage() {
'break-word',
)}
>
<IconButton
label={'Delete Document'}
icon={IoMdTrash}
className={clsx('self-end -m-2')}
size={28}
onClick={(e) => {
e.preventDefault();
// TODO: Replace with modal
if (confirm(`Are you sure you want to delete ${doc.name}?`)) {
// mutate marks the document list as stale, so SWR refreshes it
deleteDocument({ document_id: doc.id }).then(() => mutate());
}
}}
/>
<div className="w-full flex flex-row items-center justify-between">
{getTaskProgress(doc.tasks) < 1 ? (
<WorkerStatusWithData data={doc.tasks} />
) : (
<div></div>
)}
<IconButton
label={'Delete Document'}
icon={IoMdTrash}
className={clsx('self-end -m-2')}
size={28}
onClick={(e) => {
e.preventDefault();
// TODO: Replace with modal
if (confirm(`Are you sure you want to delete ${doc.name}?`)) {
// mutate marks the document list as stale, so SWR refreshes it
deleteDocument({ document_id: doc.id }).then(() => mutate());
}
}}
/>
</div>
{doc.name}
</Link>
</li>
Expand Down

0 comments on commit 3c46ae4

Please sign in to comment.