Skip to content

Commit

Permalink
feat(dashboard): Add feature to remove action events (#688)
Browse files Browse the repository at this point in the history
* feat: Add feature to remove action events

* fix: Fix database revision
  • Loading branch information
KIRA009 authored Jun 5, 2024
1 parent e0995c9 commit f33b36d
Show file tree
Hide file tree
Showing 13 changed files with 196 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""add_disabled_field_to_action_event
Revision ID: a29b537fabe6
Revises: 98c8851a5321
Create Date: 2024-05-28 11:28:50.353928
"""
from alembic import op
import sqlalchemy as sa

# revision identifiers, used by Alembic.
revision = "a29b537fabe6"
down_revision = "98c8851a5321"
branch_labels = None
depends_on = None


def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("action_event", schema=None) as batch_op:
batch_op.add_column(sa.Column("disabled", sa.Boolean(), nullable=True))

# ### end Alembic commands ###


def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
with op.batch_alter_table("action_event", schema=None) as batch_op:
batch_op.drop_column("disabled")

# ### end Alembic commands ###
42 changes: 42 additions & 0 deletions openadapt/app/dashboard/api/action_events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""API endpoints for recordings."""

from fastapi import APIRouter
from loguru import logger

from openadapt.db import crud


class ActionEventsAPI:
"""API endpoints for action events."""

def __init__(self) -> None:
"""Initialize the ActionEventsAPI class."""
self.app = APIRouter()

def attach_routes(self) -> APIRouter:
"""Attach routes to the FastAPI app."""
self.app.add_api_route("/{event_id}", self.disable_event, methods=["DELETE"])
return self.app

@staticmethod
def disable_event(event_id: int) -> dict[str, str]:
"""Disable an action event.
Args:
event_id (int): The ID of the event to disable.
Returns:
dict: The response message and status code.
"""
if not crud.acquire_db_lock():
return {"message": "Database is locked", "status": "error"}
session = crud.get_new_session(read_and_write=True)
try:
crud.disable_action_event(session, event_id)
except Exception as e:
logger.error(f"Error deleting event: {e}")
session.rollback()
crud.release_db_lock()
return {"message": "Error deleting event", "status": "error"}
crud.release_db_lock()
return {"message": "Event deleted", "status": "success"}
3 changes: 3 additions & 0 deletions openadapt/app/dashboard/api/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from loguru import logger
import uvicorn

from openadapt.app.dashboard.api.action_events import ActionEventsAPI
from openadapt.app.dashboard.api.recordings import RecordingsAPI
from openadapt.app.dashboard.api.scrubbing import ScrubbingAPI
from openadapt.app.dashboard.api.settings import SettingsAPI
Expand All @@ -20,10 +21,12 @@

api = APIRouter()

action_events_app = ActionEventsAPI().attach_routes()
recordings_app = RecordingsAPI().attach_routes()
scrubbing_app = ScrubbingAPI().attach_routes()
settings_app = SettingsAPI().attach_routes()

api.include_router(action_events_app, prefix="/action-events")
api.include_router(recordings_app, prefix="/recordings")
api.include_router(scrubbing_app, prefix="/scrubbing")
api.include_router(settings_app, prefix="/settings")
Expand Down
5 changes: 4 additions & 1 deletion openadapt/app/dashboard/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ColorSchemeScript, MantineProvider } from '@mantine/core'
import { Notifications } from '@mantine/notifications';
import { Shell } from '@/components/Shell'
import { CSPostHogProvider } from './providers';
import { ModalsProvider } from '@mantine/modals';

export const metadata = {
title: 'OpenAdapt.AI',
Expand All @@ -23,7 +24,9 @@ export default function RootLayout({
<body>
<MantineProvider>
<Notifications />
<Shell>{children}</Shell>
<ModalsProvider>
<Shell>{children}</Shell>
</ModalsProvider>
</MantineProvider>
</body>
</CSPostHogProvider>
Expand Down
10 changes: 7 additions & 3 deletions openadapt/app/dashboard/app/recordings/detail/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function Recording() {
if (!prev) return prev;
return {
...prev,
"action_events": [...prev.action_events, addIdToNullActionEvent(data.value)],
"action_events": [...prev.action_events, modifyActionEvent(data.value, prev.recording.original_recording_id === null)],
}
});
} else if (data.type === "num_events") {
Expand Down Expand Up @@ -77,22 +77,26 @@ function Recording() {
)
}

function addIdToNullActionEvent(actionEvent: ActionEventType): ActionEventType {
function modifyActionEvent(actionEvent: ActionEventType, isOriginal: boolean): ActionEventType {
let children = actionEvent.children;
if (actionEvent.children) {
children = actionEvent.children.map(addIdToNullActionEvent);
children = actionEvent.children.map(child => modifyActionEvent(child, isOriginal));
}
let id = actionEvent.id;
let isComputed = false;
if (!id) {
// this is usually the case, when new events like 'singleclick'
// or 'doubleclick' are created while merging several events together,
// but they are not saved in the database
id = crypto.randomUUID();
isComputed = true;
}
return {
...actionEvent,
id,
children,
isComputed,
isOriginal,
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { timeStampToDateString } from '@/app/utils';
import { ActionEvent as ActionEventType } from '@/types/action-event'
import { Accordion, Box, Grid, Image, Table } from '@mantine/core'
import { useHover } from '@mantine/hooks';
import { RemoveActionEvent } from './RemoveActionEvent';

type Props = {
event: ActionEventType;
Expand Down Expand Up @@ -38,8 +39,8 @@ export const ActionEvent = ({
const imageSrc = (hoveredOverScreenshot ? event.screenshot : event.screenshot) || ''; // change to event.diff to show diff

let content = (
<Grid>
<Grid.Col span={level === 0 ? 4 : 12}>
<Grid align='center'>
<Grid.Col span={8}>
<Table w={400} withTableBorder withColumnBorders my={20} className='border-2 border-gray-300 border-solid'>
<Table.Tbody>
{typeof event.id === 'number' && (
Expand Down Expand Up @@ -135,6 +136,9 @@ export const ActionEvent = ({
</Table.Tbody>
</Table>
</Grid.Col>
<Grid.Col span={4}>
<RemoveActionEvent event={event} />
</Grid.Col>
{level === 0 && (
<Grid.Col span={12}>
{event.screenshot !== null && (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { ActionEvent } from '@/types/action-event';
import { Button, Text } from '@mantine/core';
import { modals } from '@mantine/modals';
import { notifications } from '@mantine/notifications';
import React from 'react'

type Props = {
event: ActionEvent;
}

export const RemoveActionEvent = ({
event
}: Props) => {
const openModal = (e: React.MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
modals.openConfirmModal({
title: 'Please confirm your action',
children: (
<Text size="sm">
Are you sure you want to delete this action event? This action cannot be undone.
</Text>
),
labels: { confirm: 'Confirm', cancel: 'Cancel' },
onCancel: () => {},
onConfirm: deleteActionEvent,
confirmProps: { color: 'red' },
});
}

const deleteActionEvent = () => {
fetch(`/api/action-events/${event.id}`, {
method: 'DELETE',
}).then(res => res.json()).then(data => {
const { message, status } = data;
if (status === 'success') {
window.location.reload();
} else {
notifications.show({
title: 'Error',
message,
color: 'red',
})
}
});
}
if (event.isComputed || !event.isOriginal) return null;
return (
<Button variant='filled' color='red' onClick={openModal}>
Remove action event
</Button>
)
}
12 changes: 12 additions & 0 deletions openadapt/app/dashboard/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions openadapt/app/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@mantine/core": "7.7.1",
"@mantine/form": "7.7.1",
"@mantine/hooks": "7.7.1",
"@mantine/modals": "^7.7.1",
"@mantine/notifications": "7.7.1",
"@tabler/icons-react": "^3.1.0",
"@types/node": "20.2.4",
Expand Down
2 changes: 2 additions & 0 deletions openadapt/app/dashboard/types/action-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ export type ActionEvent = {
dimensions?: { width: number, height: number };
children?: ActionEvent[];
words?: string[];
isComputed?: boolean;
isOriginal?: boolean;
}
1 change: 1 addition & 0 deletions openadapt/app/dashboard/types/recording.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export type Recording = {
platform: string;
task_description: string;
video_start_time: number | null;
original_recording_id: number | null;
}

export enum RecordingStatus {
Expand Down
48 changes: 34 additions & 14 deletions openadapt/db/crud.py
Original file line number Diff line number Diff line change
Expand Up @@ -392,27 +392,24 @@ def get_action_events(
"""
assert recording, "Invalid recording."
action_events = _get(session, ActionEvent, recording.id)
action_events = filter_disabled_action_events(action_events)
# filter out stop sequences listed in STOP_SEQUENCES and Ctrl + C
filter_stop_sequences(action_events)
return action_events


def get_top_level_action_events(
recording: Recording, session: sa.orm.Session = None
def filter_disabled_action_events(
action_events: list[ActionEvent],
) -> list[ActionEvent]:
"""Get top level action events for a given recording.
"""Filter out disabled action events.
Args:
recording (Recording): The recording object.
action_events (list[ActionEvent]): A list of action events.
Returns:
list[ActionEvent]: A list of top level action events for the recording.
list[ActionEvent]: A list of action events with disabled events removed.
"""
return [
action_event
for action_event in get_action_events(recording, session=session)
if not action_event.parent_id
]
return [event for event in action_events if not event.disabled]


def filter_stop_sequences(action_events: list[ActionEvent]) -> None:
Expand Down Expand Up @@ -561,6 +558,24 @@ def get_window_events(
return _get(session, WindowEvent, recording.id)


def disable_action_event(session: SaSession, event_id: int) -> None:
"""Disable an action event.
Args:
session (sa.orm.Session): The database session.
event_id (int): The id of the event.
"""
action_event: ActionEvent = (
session.query(ActionEvent).filter(ActionEvent.id == event_id).first()
)
if action_event.recording.original_recording_id:
raise ValueError("Cannot disable action events in a scrubbed recording.")
if not action_event:
raise ValueError(f"No action event found with id {event_id}.")
action_event.disabled = True
session.commit()


def get_new_session(
read_only: bool = False,
read_and_write: bool = False,
Expand Down Expand Up @@ -841,11 +856,16 @@ def acquire_db_lock(timeout: int = 60) -> bool:


def release_db_lock(raise_exception: bool = True) -> None:
"""Release the database lock."""
"""Release the database lock.
Args:
raise_exception (bool): Whether to raise an exception if the lock file is
not found.
"""
try:
os.remove(DATABASE_LOCK_FILE_PATH)
except Exception as e:
except FileNotFoundError:
if raise_exception:
logger.error("Failed to release database lock.")
raise e
logger.error("Database lock file not found.")
raise
logger.info("Database lock released.")
1 change: 1 addition & 0 deletions openadapt/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ class ActionEvent(db.Base):
canonical_key_vk = sa.Column(sa.String)
parent_id = sa.Column(sa.Integer, sa.ForeignKey("action_event.id"))
element_state = sa.Column(sa.JSON)
disabled = sa.Column(sa.Boolean, default=False)

scrubbed_text = sa.Column(sa.String)
scrubbed_canonical_text = sa.Column(sa.String)
Expand Down

0 comments on commit f33b36d

Please sign in to comment.