✨ (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:
parent
f79289f966
commit
602ebf7b15
12 changed files with 220 additions and 85 deletions
|
|
@ -2,6 +2,7 @@ from typing import List, Optional
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
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.deps import get_monitor_service
|
||||||
from langflow.services.monitor.schema import (
|
from langflow.services.monitor.schema import (
|
||||||
MessageModelRequest,
|
MessageModelRequest,
|
||||||
|
|
@ -67,13 +68,13 @@ async def get_messages(
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/messages", status_code=204)
|
@router.post("/messages", status_code=204)
|
||||||
async def delete_messages(
|
async def delete_messages(
|
||||||
message_ids: List[int],
|
message_ids: MessageIds,
|
||||||
monitor_service: MonitorService = Depends(get_monitor_service),
|
monitor_service: MonitorService = Depends(get_monitor_service),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
monitor_service.delete_messages(message_ids=message_ids)
|
monitor_service.delete_messages(message_ids=message_ids.ids)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=str(e))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -321,3 +321,6 @@ class FlowDataRequest(BaseModel):
|
||||||
|
|
||||||
class ConfigResponse(BaseModel):
|
class ConfigResponse(BaseModel):
|
||||||
frontend_timeout: int
|
frontend_timeout: int
|
||||||
|
|
||||||
|
class MessageIds(BaseModel):
|
||||||
|
ids: List[int]
|
||||||
|
|
@ -108,7 +108,7 @@ class MonitorService(Service):
|
||||||
return self.exec_query(query)
|
return self.exec_query(query)
|
||||||
|
|
||||||
def delete_messages(self, message_ids: list[int]):
|
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)
|
return self.exec_query(query)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
import "ag-grid-community/styles/ag-grid.css"; // Mandatory CSS required by the grid
|
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 "ag-grid-community/styles/ag-theme-quartz.css"; // Optional Theme applied to the grid
|
||||||
import { AgGridReact, AgGridReactProps } from "ag-grid-react";
|
import { AgGridReact, AgGridReactProps } from "ag-grid-react";
|
||||||
import { ElementRef, forwardRef, useCallback } from "react";
|
import { ElementRef, forwardRef } from "react";
|
||||||
import {
|
import {
|
||||||
DEFAULT_TABLE_ALERT_MSG,
|
DEFAULT_TABLE_ALERT_MSG,
|
||||||
DEFAULT_TABLE_ALERT_TITLE,
|
DEFAULT_TABLE_ALERT_TITLE,
|
||||||
} from "../../constants/constants";
|
} from "../../constants/constants";
|
||||||
import { useDarkStore } from "../../stores/darkStore";
|
import { useDarkStore } from "../../stores/darkStore";
|
||||||
import "../../style/ag-theme-shadcn.css"; // Custom CSS applied to the grid
|
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 ForwardedIconComponent from "../genericIconComponent";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "../ui/alert";
|
||||||
|
|
||||||
|
|
@ -46,16 +46,21 @@ const TableComponent = forwardRef<
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const colDef = props.columnDefs.map((col, index) => {
|
const colDef = props.columnDefs.map((col, index) => {
|
||||||
if (props.onSelectionChanged && index === 0) {
|
if (props.onSelectionChanged && index === 0) {
|
||||||
return {
|
return {
|
||||||
...col,
|
...col,
|
||||||
|
headerName: toTitleCase(col.headerName),
|
||||||
checkboxSelection: true,
|
checkboxSelection: true,
|
||||||
headerCheckboxSelection: true,
|
headerCheckboxSelection: true,
|
||||||
headerCheckboxSelectionFilteredOnly: true,
|
headerCheckboxSelectionFilteredOnly: true,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
return col;
|
return {
|
||||||
|
...col,
|
||||||
|
headerName: toTitleCase(col.headerName),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1026,7 +1026,7 @@ export async function getMessagesTable(
|
||||||
id?: string,
|
id?: string,
|
||||||
excludedFields?: string[],
|
excludedFields?: string[],
|
||||||
params = {},
|
params = {},
|
||||||
): Promise<{ rows: Array<object>; columns: Array<ColDef | ColGroupDef> }> {
|
): Promise<{ rows: Array<Message>; columns: Array<ColDef | ColGroupDef> }> {
|
||||||
const config = {};
|
const config = {};
|
||||||
if (id) {
|
if (id) {
|
||||||
config["params"] = { flow_id: id };
|
config["params"] = { flow_id: id };
|
||||||
|
|
@ -1038,3 +1038,9 @@ export async function getMessagesTable(
|
||||||
const columns = extractColumnsFromRows(rows.data, mode, excludedFields);
|
const columns = extractColumnsFromRows(rows.data, mode, excludedFields);
|
||||||
return { rows: rows.data, columns };
|
return { rows: rows.data, columns };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteMessagesFn(ids: number[]) {
|
||||||
|
return await api.post(`${BASE_URL_API}monitor/messages`, {
|
||||||
|
ids,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -1,94 +1,40 @@
|
||||||
import IconComponent from "../../../../components/genericIconComponent";
|
|
||||||
import { Button } from "../../../../components/ui/button";
|
|
||||||
|
|
||||||
import { ColDef, ColGroupDef, SelectionChangedEvent } from "ag-grid-community";
|
import { ColDef, ColGroupDef, SelectionChangedEvent } from "ag-grid-community";
|
||||||
import { useEffect, useState } from "react";
|
import { useState } from "react";
|
||||||
import AddNewVariableButton from "../../../../components/addNewVariableButtonComponent/addNewVariableButton";
|
|
||||||
import Dropdown from "../../../../components/dropdownComponent";
|
|
||||||
import ForwardedIconComponent from "../../../../components/genericIconComponent";
|
|
||||||
import TableComponent from "../../../../components/tableComponent";
|
import TableComponent from "../../../../components/tableComponent";
|
||||||
import { Badge } from "../../../../components/ui/badge";
|
|
||||||
import { Card, CardContent } from "../../../../components/ui/card";
|
import { Card, CardContent } from "../../../../components/ui/card";
|
||||||
import {
|
|
||||||
deleteGlobalVariable,
|
|
||||||
getMessagesTable,
|
|
||||||
} from "../../../../controllers/API";
|
|
||||||
import useAlertStore from "../../../../stores/alertStore";
|
import useAlertStore from "../../../../stores/alertStore";
|
||||||
import { useGlobalVariablesStore } from "../../../../stores/globalVariablesStore/globalVariables";
|
import { useMessagesStore } from "../../../../stores/messagesStore";
|
||||||
import { cn } from "../../../../utils/utils";
|
import HeaderMessagesComponent from "./components/headerMessages";
|
||||||
|
import useMessagesTable from "./hooks/use-messages-table";
|
||||||
|
import useRemoveMessages from "./hooks/use-remove-messages";
|
||||||
|
|
||||||
export default function MessagesPage() {
|
export default function MessagesPage() {
|
||||||
|
const setMessages = useMessagesStore((state) => state.setMessages);
|
||||||
|
|
||||||
const [columns, setColumns] = useState<Array<ColDef | ColGroupDef>>([]);
|
const [columns, setColumns] = useState<Array<ColDef | ColGroupDef>>([]);
|
||||||
const [rows, setRows] = useState<any>([]);
|
const [rows, setRows] = useState<any>([]);
|
||||||
const removeGlobalVariable = useGlobalVariablesStore(
|
|
||||||
(state) => state.removeGlobalVariable,
|
|
||||||
);
|
|
||||||
const setErrorData = useAlertStore((state) => state.setErrorData);
|
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 { handleRemoveMessages } = useRemoveMessages(
|
||||||
const deleteGlobalVariablesPromise = selectedRows.map(async (row) => {
|
setRows,
|
||||||
const id = getVariableId(row);
|
setSelectedRows,
|
||||||
const deleteGlobalVariables = deleteGlobalVariable(id!);
|
setSuccessData,
|
||||||
await deleteGlobalVariables;
|
setErrorData,
|
||||||
});
|
selectedRows,
|
||||||
Promise.all(deleteGlobalVariablesPromise)
|
);
|
||||||
.then(() => {
|
|
||||||
selectedRows.forEach((row) => {
|
|
||||||
removeGlobalVariable(row);
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
setErrorData({
|
|
||||||
title: `Error deleting global variables.`,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useMessagesTable(setColumns, setRows, setMessages);
|
||||||
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);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex h-full w-full flex-col justify-between gap-6">
|
<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">
|
<HeaderMessagesComponent
|
||||||
<div className="flex w-full flex-col">
|
selectedRows={selectedRows}
|
||||||
<h2 className="flex items-center text-lg font-semibold tracking-tight">
|
handleRemoveMessages={handleRemoveMessages}
|
||||||
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>
|
|
||||||
|
|
||||||
<div className="flex h-full w-full flex-col justify-between pb-8">
|
<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">
|
<Card x-chunk="dashboard-04-chunk-2" className="h-full pt-4">
|
||||||
|
|
@ -97,7 +43,7 @@ export default function MessagesPage() {
|
||||||
overlayNoRowsTemplate="No data available"
|
overlayNoRowsTemplate="No data available"
|
||||||
onSelectionChanged={(event: SelectionChangedEvent) => {
|
onSelectionChanged={(event: SelectionChangedEvent) => {
|
||||||
setSelectedRows(
|
setSelectedRows(
|
||||||
event.api.getSelectedRows().map((row) => row.name),
|
event.api.getSelectedRows().map((row) => row.index),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
rowSelection="multiple"
|
rowSelection="multiple"
|
||||||
|
|
|
||||||
43
src/frontend/src/stores/messagesStore.ts
Normal file
43
src/frontend/src/stores/messagesStore.ts
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
}));
|
||||||
11
src/frontend/src/types/messages/index.ts
Normal file
11
src/frontend/src/types/messages/index.ts
Normal 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;
|
||||||
|
};
|
||||||
9
src/frontend/src/types/zustand/messages/index.ts
Normal file
9
src/frontend/src/types/zustand/messages/index.ts
Normal 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[]>;
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue