(monitor.py): add MessageIds schema for structured message deletion

♻️ (monitor.py): change delete_messages endpoint to POST for better semantics
♻️ (monitor.py): update delete_messages to use MessageIds schema
 (schemas.py): add MessageIds schema for structured message deletion
🐛 (service.py): fix SQL query in delete_messages to use correct column name
 (index.tsx): add toTitleCase utility to format column headers
 (API/index.ts): add deleteMessagesFn to handle message deletion via API
 (headerMessages): add HeaderMessagesComponent for message management UI
 (use-messages-table): add useMessagesTable hook to fetch and manage messages
 (use-remove-messages): add useRemoveMessages hook to handle message deletion

♻️ (messagesPage): refactor messages page to use new messages store
 (messagesStore): create zustand store for managing messages state
 (types): add types for messages and zustand messages store
This commit is contained in:
cristhianzl 2024-05-31 13:43:28 -03:00
commit 602ebf7b15
12 changed files with 220 additions and 85 deletions

View file

@ -2,6 +2,7 @@ from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from langflow.api.v1.schemas import MessageIds
from langflow.services.deps import get_monitor_service
from langflow.services.monitor.schema import (
MessageModelRequest,
@ -67,13 +68,13 @@ async def get_messages(
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/messages", status_code=204)
@router.post("/messages", status_code=204)
async def delete_messages(
message_ids: List[int],
message_ids: MessageIds,
monitor_service: MonitorService = Depends(get_monitor_service),
):
try:
monitor_service.delete_messages(message_ids=message_ids)
monitor_service.delete_messages(message_ids=message_ids.ids)
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))

View file

@ -321,3 +321,6 @@ class FlowDataRequest(BaseModel):
class ConfigResponse(BaseModel):
frontend_timeout: int
class MessageIds(BaseModel):
ids: List[int]

View file

@ -108,7 +108,7 @@ class MonitorService(Service):
return self.exec_query(query)
def delete_messages(self, message_ids: list[int]):
query = f"DELETE FROM messages WHERE id IN ({','.join(str(message_ids))})"
query = f"DELETE FROM messages WHERE index IN ({','.join(map(str, message_ids))})"
return self.exec_query(query)

View file

@ -1,14 +1,14 @@
import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid
import "ag-grid-community/styles/ag-theme-quartz.css"; // Optional Theme applied to the grid
import { AgGridReact, AgGridReactProps } from "ag-grid-react";
import { ElementRef, forwardRef, useCallback } from "react";
import { ElementRef, forwardRef } from "react";
import {
DEFAULT_TABLE_ALERT_MSG,
DEFAULT_TABLE_ALERT_TITLE,
} from "../../constants/constants";
import { useDarkStore } from "../../stores/darkStore";
import "../../style/ag-theme-shadcn.css"; // Custom CSS applied to the grid
import { cn } from "../../utils/utils";
import { cn, toTitleCase } from "../../utils/utils";
import ForwardedIconComponent from "../genericIconComponent";
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
@ -46,16 +46,21 @@ const TableComponent = forwardRef<
</div>
);
}
const colDef = props.columnDefs.map((col, index) => {
if (props.onSelectionChanged && index === 0) {
return {
...col,
headerName: toTitleCase(col.headerName),
checkboxSelection: true,
headerCheckboxSelection: true,
headerCheckboxSelectionFilteredOnly: true,
};
} else {
return col;
return {
...col,
headerName: toTitleCase(col.headerName),
};
}
});

View file

@ -1026,7 +1026,7 @@ export async function getMessagesTable(
id?: string,
excludedFields?: string[],
params = {},
): Promise<{ rows: Array<object>; columns: Array<ColDef | ColGroupDef> }> {
): Promise<{ rows: Array<Message>; columns: Array<ColDef | ColGroupDef> }> {
const config = {};
if (id) {
config["params"] = { flow_id: id };
@ -1038,3 +1038,9 @@ export async function getMessagesTable(
const columns = extractColumnsFromRows(rows.data, mode, excludedFields);
return { rows: rows.data, columns };
}
export async function deleteMessagesFn(ids: number[]) {
return await api.post(`${BASE_URL_API}monitor/messages`, {
ids,
});
}

View file

@ -0,0 +1,48 @@
import ForwardedIconComponent from "../../../../../../components/genericIconComponent";
import { Button } from "../../../../../../components/ui/button";
import { cn } from "../../../../../../utils/utils";
type HeaderMessagesComponentProps = {
selectedRows: number[];
handleRemoveMessages: () => void;
};
const HeaderMessagesComponent = ({
selectedRows,
handleRemoveMessages,
}: HeaderMessagesComponentProps) => {
return (
<>
<div className="flex w-full items-center justify-between gap-4 space-y-0.5">
<div className="flex w-full flex-col">
<h2 className="flex items-center text-lg font-semibold tracking-tight">
Messages
<ForwardedIconComponent
name="MessagesSquare"
className="ml-2 h-5 w-5 text-primary"
/>
</h2>
<p className="text-sm text-muted-foreground">
Manage your messages as you like.
</p>
</div>
<div className="flex flex-shrink-0 items-center gap-2">
<Button
data-testid="api-key-button-store"
variant="primary"
className="group px-2"
disabled={selectedRows.length === 0}
onClick={handleRemoveMessages}
>
<ForwardedIconComponent
name="Trash2"
className={cn(
"h-5 w-5 text-destructive group-disabled:text-primary",
)}
/>
</Button>
</div>
</div>
</>
);
};
export default HeaderMessagesComponent;

View file

@ -0,0 +1,23 @@
import { useEffect } from "react";
import { getMessagesTable } from "../../../../../controllers/API";
const useMessagesTable = (setColumns, setRows, setMessages) => {
useEffect(() => {
const fetchData = async () => {
try {
const data = await getMessagesTable("union", undefined, ["index"]);
const { columns, rows } = data;
setColumns(columns.map((col) => ({ ...col, editable: true })));
setRows(rows);
setMessages(rows);
} catch (error) {
console.error("Error fetching messages:", error);
}
};
fetchData();
}, []);
return null;
};
export default useMessagesTable;

View file

@ -0,0 +1,40 @@
import { deleteMessagesFn } from "../../../../../controllers/API";
import { useMessagesStore } from "../../../../../stores/messagesStore";
const useRemoveMessages = (
setRows,
setSelectedRows,
setSuccessData,
setErrorData,
selectedRows,
) => {
const deleteMessages = useMessagesStore((state) => state.removeMessages);
const handleRemoveMessages = async () => {
try {
// Call the deleteMessagesFn to perform the deletion
await deleteMessagesFn(selectedRows);
// Assuming deleteMessages is a separate function that updates state after deletion
const res = await deleteMessages(selectedRows);
setRows(res);
// Clear the selected rows
setSelectedRows([]);
// Set success message
setSuccessData({
title: "Messages deleted successfully.",
});
} catch (error) {
// Set error message
setErrorData({
title: "Error deleting messages.",
});
}
};
return { handleRemoveMessages };
};
export default useRemoveMessages;

View file

@ -1,94 +1,40 @@
import IconComponent from "../../../../components/genericIconComponent";
import { Button } from "../../../../components/ui/button";
import { ColDef, ColGroupDef, SelectionChangedEvent } from "ag-grid-community";
import { useEffect, useState } from "react";
import AddNewVariableButton from "../../../../components/addNewVariableButtonComponent/addNewVariableButton";
import Dropdown from "../../../../components/dropdownComponent";
import ForwardedIconComponent from "../../../../components/genericIconComponent";
import { useState } from "react";
import TableComponent from "../../../../components/tableComponent";
import { Badge } from "../../../../components/ui/badge";
import { Card, CardContent } from "../../../../components/ui/card";
import {
deleteGlobalVariable,
getMessagesTable,
} from "../../../../controllers/API";
import useAlertStore from "../../../../stores/alertStore";
import { useGlobalVariablesStore } from "../../../../stores/globalVariablesStore/globalVariables";
import { cn } from "../../../../utils/utils";
import { useMessagesStore } from "../../../../stores/messagesStore";
import HeaderMessagesComponent from "./components/headerMessages";
import useMessagesTable from "./hooks/use-messages-table";
import useRemoveMessages from "./hooks/use-remove-messages";
export default function MessagesPage() {
const setMessages = useMessagesStore((state) => state.setMessages);
const [columns, setColumns] = useState<Array<ColDef | ColGroupDef>>([]);
const [rows, setRows] = useState<any>([]);
const removeGlobalVariable = useGlobalVariablesStore(
(state) => state.removeGlobalVariable,
);
const setErrorData = useAlertStore((state) => state.setErrorData);
const getVariableId = useGlobalVariablesStore((state) => state.getVariableId);
const setSuccessData = useAlertStore((state) => state.setSuccessData);
const [selectedRows, setSelectedRows] = useState<string[]>([]);
const [selectedRows, setSelectedRows] = useState<number[]>([]);
async function removeVariables() {
const deleteGlobalVariablesPromise = selectedRows.map(async (row) => {
const id = getVariableId(row);
const deleteGlobalVariables = deleteGlobalVariable(id!);
await deleteGlobalVariables;
});
Promise.all(deleteGlobalVariablesPromise)
.then(() => {
selectedRows.forEach((row) => {
removeGlobalVariable(row);
});
})
.catch(() => {
setErrorData({
title: `Error deleting global variables.`,
});
});
}
const { handleRemoveMessages } = useRemoveMessages(
setRows,
setSelectedRows,
setSuccessData,
setErrorData,
selectedRows,
);
useEffect(() => {
console.log("MessagesPage useEffect");
getMessagesTable("union", undefined, ["index"]).then((data) => {
const { columns, rows } = data;
console.log(data);
setColumns(columns.map((col) => ({ ...col, editable: true })));
setRows(rows);
});
}, []);
useMessagesTable(setColumns, setRows, setMessages);
return (
<div className="flex h-full w-full flex-col justify-between gap-6">
<div className="flex w-full items-center justify-between gap-4 space-y-0.5">
<div className="flex w-full flex-col">
<h2 className="flex items-center text-lg font-semibold tracking-tight">
Messages
<ForwardedIconComponent
name="MessagesSquare"
className="ml-2 h-5 w-5 text-primary"
/>
</h2>
<p className="text-sm text-muted-foreground">
Manage your messages as you like.
</p>
</div>
<div className="flex flex-shrink-0 items-center gap-2">
<Button
data-testid="api-key-button-store"
variant="primary"
className="group px-2"
disabled={selectedRows.length === 0}
onClick={removeVariables}
>
<IconComponent
name="Trash2"
className={cn(
"h-5 w-5 text-destructive group-disabled:text-primary",
)}
/>
</Button>
</div>
</div>
<HeaderMessagesComponent
selectedRows={selectedRows}
handleRemoveMessages={handleRemoveMessages}
/>
<div className="flex h-full w-full flex-col justify-between pb-8">
<Card x-chunk="dashboard-04-chunk-2" className="h-full pt-4">
@ -97,7 +43,7 @@ export default function MessagesPage() {
overlayNoRowsTemplate="No data available"
onSelectionChanged={(event: SelectionChangedEvent) => {
setSelectedRows(
event.api.getSelectedRows().map((row) => row.name),
event.api.getSelectedRows().map((row) => row.index),
);
}}
rowSelection="multiple"

View file

@ -0,0 +1,43 @@
import { create } from "zustand";
import { MessagesStoreType } from "../types/zustand/messages";
export const useMessagesStore = create<MessagesStoreType>((set, get) => ({
messages: [],
setMessages: (messages) => {
set(() => ({ messages: messages }));
},
addMessage: (message) => {
set(() => ({ messages: [...get().messages, message] }));
},
removeMessage: (message) => {
set(() => ({
messages: get().messages.filter((msg) => msg.id !== message.id),
}));
},
updateMessage: (message) => {
set(() => ({
messages: get().messages.map((msg) =>
msg.id === message.id ? message : msg,
),
}));
},
clearMessages: () => {
set(() => ({ messages: [] }));
},
removeMessages: (ids) => {
return new Promise((resolve, reject) => {
try {
set((state) => {
const updatedMessages = state.messages.filter(
(msg) => !ids.includes(msg.index),
);
get().setMessages(updatedMessages);
resolve(updatedMessages);
return { messages: updatedMessages };
});
} catch (error) {
reject(error);
}
});
},
}));

View file

@ -0,0 +1,11 @@
type Message = {
artifacts: Record<string, any>;
flow_id: string;
index: number;
message: string;
sender: string;
sender_name: string;
session_id: string;
timestamp: string;
id: string;
};

View file

@ -0,0 +1,9 @@
export type MessagesStoreType = {
messages: Message[];
setMessages: (messages: Message[]) => void;
addMessage: (message: Message) => void;
removeMessage: (message: Message) => void;
updateMessage: (message: Message) => void;
clearMessages: () => void;
removeMessages: (ids: number[]) => Promise<Message[]>;
};