diff --git a/agenta-web/src/components/TestSetTable/TableCellsRenderer.tsx b/agenta-web/src/components/TestSetTable/TableCellsRenderer.tsx new file mode 100644 index 000000000..9c7461329 --- /dev/null +++ b/agenta-web/src/components/TestSetTable/TableCellsRenderer.tsx @@ -0,0 +1,55 @@ +import {Tooltip} from "antd" +import {createUseStyles} from "react-jss" +import {EditOutlined} from "@ant-design/icons" +import {ICellRendererParams} from "ag-grid-community" + +const useStylesCell = createUseStyles({ + cellContainer: { + position: "relative", + display: "flex", + alignItems: "center", + gap: 2, + height: "100%", + + "&:hover>:nth-child(2)": { + display: "inline", + }, + }, + cellValue: { + whiteSpace: "nowrap", + overflow: "hidden", + textOverflow: "ellipsis", + flex: 1, + }, + cellEditIcon: { + display: "none", + }, +}) + +const TableCellsRenderer = (props: ICellRendererParams) => { + const classes = useStylesCell() + const cellValue = props.valueFormatted ? props.valueFormatted : props.value + + return props.colDef?.field ? ( + + props.api.startEditingCell({ + rowIndex: props.node.rowIndex as number, + colKey: props.colDef?.field as string, + }) + } + > + {cellValue || ""} + + + props.colDef?.cellRendererParams?.onEdit(props.rowIndex)} + /> + + + + ) : undefined +} + +export default TableCellsRenderer diff --git a/agenta-web/src/components/TestSetTable/TableHeaderComponent.tsx b/agenta-web/src/components/TestSetTable/TableHeaderComponent.tsx new file mode 100644 index 000000000..943737cb0 --- /dev/null +++ b/agenta-web/src/components/TestSetTable/TableHeaderComponent.tsx @@ -0,0 +1,188 @@ +import {useState, useEffect} from "react" +import {DeleteOutlined, EditOutlined, PlusOutlined} from "@ant-design/icons" +import {Button, Input, message} from "antd" +import {createUseStyles} from "react-jss" +import {ADD_BUTTON_COL} from "./TestsetTable" +import {KeyValuePair} from "@/lib/Types" +import {ColumnDefsType} from "./TestsetTable" + +const useStylesTestset = createUseStyles({ + plusIcon: { + width: "100%", + display: "flex", + justifyContent: "end", + "& button": { + marginRight: "10px", + }, + }, + columnTitle: { + width: "100%", + height: "100% ", + display: "flex", + alignItems: "center", + justifyContent: "space-between", + "& input": { + marginTop: "10px", + marginBottom: "10px", + height: "30px", + marginRight: "3px", + outline: "red", + }, + }, + saveBtn: { + width: "45px !important", + }, +}) + +type TableHeaderComponentProps = { + params: any + columnDefs: ColumnDefsType[] + inputValues: string[] + rowData: KeyValuePair[] + setColumnDefs: React.Dispatch> + setInputValues: React.Dispatch> + setRowData: React.Dispatch> + setIsDataChanged: React.Dispatch> + updateTable: (inputValues: string[]) => void + onDeleteColumn: (indexToDelete: number) => void +} + +const TableHeaderComponent = ({ + params, + columnDefs, + inputValues, + rowData, + setColumnDefs, + setInputValues, + setRowData, + setIsDataChanged, + updateTable, + onDeleteColumn, +}: TableHeaderComponentProps) => { + const [scopedInputValues, setScopedInputValues] = useState( + columnDefs.filter((colDef) => colDef.field !== "").map((col) => col.field), + ) + const [isEditInputOpen, setIsEditInputOpen] = useState(false) + + const {attributes} = params.eGridHeader + const index = attributes["aria-colindex"].nodeValue - 2 + const displayName = params.displayName + + const classes = useStylesTestset() + + useEffect(() => { + setScopedInputValues(inputValues) + }, [columnDefs]) + + useEffect(() => { + const handleEscape = (e: KeyboardEvent) => { + if (e.key == "Enter") { + if (isEditInputOpen) { + handleSave() + } + } + } + window.addEventListener("keydown", handleEscape) + return () => window.removeEventListener("keydown", handleEscape) + }, [isEditInputOpen, scopedInputValues]) + + const handleSave = () => { + if (scopedInputValues[index] == inputValues[index]) { + setIsEditInputOpen(false) + return + } + + if ( + inputValues.some((input) => input === scopedInputValues[index]) || + scopedInputValues[index] == "" + ) { + message.error( + scopedInputValues[index] == "" + ? "Invalid column name" + : "Column name already exist!", + ) + } else { + setInputValues(scopedInputValues) + updateTable(scopedInputValues) + setIsEditInputOpen(false) + } + } + + const handleInputChange = (index: number, event: React.ChangeEvent) => { + const values = [...inputValues] + values[index] = event.target.value + setScopedInputValues(values) + setIsDataChanged(true) + } + + const onAddColumn = () => { + const newColumnName = `column${columnDefs.length - 1}` + const newColmnDef = columnDefs + const updatedRowData = rowData.map((row) => ({ + ...row, + [newColumnName]: "", + })) + + newColmnDef.pop() + + setInputValues([...inputValues, newColumnName]) + setColumnDefs([ + ...columnDefs, + {field: newColumnName, headerName: newColumnName}, + ADD_BUTTON_COL, + ]) + setRowData(updatedRowData) + setIsDataChanged(true) + } + + if (displayName === "" && params.column?.colId !== "0") { + return ( +
+ +
+ ) + } else if (displayName === "" && params.column?.colId === "0") { + return null + } else { + return ( +
+ {isEditInputOpen ? ( + handleInputChange(index, event)} + size="small" + /> + ) : ( + displayName + )} + +
+ {isEditInputOpen ? ( +
+
+ ) + } +} + +export default TableHeaderComponent diff --git a/agenta-web/src/components/TestSetTable/TestsetTable.tsx b/agenta-web/src/components/TestSetTable/TestsetTable.tsx index 936fc8189..75990212b 100644 --- a/agenta-web/src/components/TestSetTable/TestsetTable.tsx +++ b/agenta-web/src/components/TestSetTable/TestsetTable.tsx @@ -1,9 +1,9 @@ import React, {useState, useRef, useEffect, ReactNode} from "react" import {AgGridReact} from "ag-grid-react" +import {IHeaderParams} from "ag-grid-community" import {createUseStyles} from "react-jss" -import {Button, Input, Tooltip, Typography, message} from "antd" +import {Button, Input, Typography, message} from "antd" import TestsetMusHaveNameModal from "./InsertTestsetNameModal" -import {DeleteOutlined, EditOutlined, PlusOutlined} from "@ant-design/icons" import {fetchVariants} from "@/services/api" import {createNewTestset, fetchTestset, updateTestset} from "@/services/testsets/api" import {useRouter} from "next/router" @@ -17,10 +17,13 @@ import {getVariantInputParameters} from "@/lib/helpers/variantHelper" import {convertToCsv, downloadCsv} from "@/lib/helpers/fileManipulations" import {NoticeType} from "antd/es/message/interface" import {GenericObject, KeyValuePair} from "@/lib/Types" +import TableCellsRenderer from "./TableCellsRenderer" +import TableHeaderComponent from "./TableHeaderComponent" -type testsetTableProps = { +type TestsetTableProps = { mode: "create" | "edit" } +export type ColumnDefsType = {field: string; [key: string]: any} export const CHECKBOX_COL = { field: "", @@ -33,55 +36,7 @@ export const CHECKBOX_COL = { export const ADD_BUTTON_COL = {field: "", editable: false, maxWidth: 100} -const useStylesCell = createUseStyles({ - cellContainer: { - position: "relative", - display: "flex", - alignItems: "center", - gap: 2, - height: "100%", - - "&:hover>:nth-child(2)": { - display: "inline", - }, - }, - cellValue: { - whiteSpace: "nowrap", - overflow: "hidden", - textOverflow: "ellipsis", - flex: 1, - }, - cellEditIcon: { - display: "none", - }, -}) - const useStylesTestset = createUseStyles({ - plusIcon: { - width: "100%", - display: "flex", - justifyContent: "end", - "& button": { - marginRight: "10px", - }, - }, - columnTitle: { - width: "100%", - height: "100% ", - display: "flex", - alignItems: "center", - justifyContent: "space-between", - "& input": { - marginTop: "10px", - marginBottom: "10px", - height: "30px", - marginRight: "3px", - outline: "red", - }, - }, - saveBtn: { - width: "45px !important", - }, title: { marginBottom: "20px !important", }, @@ -97,6 +52,9 @@ const useStylesTestset = createUseStyles({ }, notes: { marginBottom: 20, + "& span": { + display: "block", + }, }, btnContainer: { display: "flex", @@ -106,33 +64,7 @@ const useStylesTestset = createUseStyles({ }, }) -function CellRenderer(props: any) { - const classes = useStylesCell() - const cellValue = props.valueFormatted ? props.valueFormatted : props.value - - return props.colDef.field ? ( - - props.api.startEditingCell({ - rowIndex: props.node.rowIndex, - colKey: props.colDef.field, - }) - } - > - {cellValue || ""} - - - props.colDef?.cellRendererParams?.onEdit(props.rowIndex)} - /> - - - - ) : undefined -} - -const TestsetTable: React.FC = ({mode}) => { +const TestsetTable: React.FC = ({mode}) => { const [messageApi, contextHolder] = message.useMessage() const mssgModal = (type: NoticeType, content: ReactNode) => { @@ -142,17 +74,13 @@ const TestsetTable: React.FC = ({mode}) => { }) } - const classes = useStylesTestset() - const router = useRouter() - const appId = router.query.app_id as string - const {testset_id} = router.query const [unSavedChanges, setUnSavedChanges] = useStateCallback(false) - const [loading, setLoading] = useState(false) + const [isDataChanged, setIsDataChanged] = useState(false) const [isLoading, setIsLoading] = useState(false) const [testsetName, setTestsetName] = useState("") const [rowData, setRowData] = useState([]) const [isModalOpen, setIsModalOpen] = useState(false) - const [columnDefs, setColumnDefs] = useState<{field: string; [key: string]: any}[]>([]) + const [columnDefs, setColumnDefs] = useState([]) const [inputValues, setInputValues] = useStateCallback(columnDefs.map((col) => col.field)) const [focusedRowData, setFocusedRowData] = useState() const [writeMode, setWriteMode] = useState(mode) @@ -161,19 +89,12 @@ const TestsetTable: React.FC = ({mode}) => { const [selectedRow, setSelectedRow] = useState([]) - const onRowSelectedOrDeselected = () => { - if (!gridRef?.current) return - setSelectedRow(gridRef?.current?.getSelectedNodes()) - } + const classes = useStylesTestset() + const router = useRouter() + const {appTheme} = useAppTheme() - const handleExportClick = () => { - const csvData = convertToCsv( - rowData, - columnDefs.map((col) => col.field), - ) - const filename = `${testsetName}.csv` - downloadCsv(csvData, filename) - } + const appId = router.query.app_id as string + const {testset_id} = router.query useBlockNavigation(unSavedChanges, { title: "Unsaved changes", @@ -188,21 +109,14 @@ const TestsetTable: React.FC = ({mode}) => { }) useUpdateEffect(() => { - if (!loading) { + if (isDataChanged) { setUnSavedChanges(true) } }, [rowData, testsetName, columnDefs, inputValues]) useEffect(() => { async function applyColData(colData: {field: string}[] = []) { - const newColDefs = [ - CHECKBOX_COL, - ...colData.map((col) => ({ - ...col, - headerName: col.field, - })), - ADD_BUTTON_COL, - ] + const newColDefs = createNewColDefs(colData) setColumnDefs(newColDefs) if (writeMode === "create") { const initialRowData = Array(3).fill({}) @@ -216,7 +130,7 @@ const TestsetTable: React.FC = ({mode}) => { } if (writeMode === "edit" && testset_id) { - setLoading(true) + setIsDataChanged(true) fetchTestset(testset_id as string).then((data) => { setTestsetName(data.name) setRowData(data.csvdata) @@ -227,7 +141,7 @@ const TestsetTable: React.FC = ({mode}) => { ) }) } else if (writeMode === "create" && appId) { - setLoading(true) + setIsDataChanged(true) ;(async () => { const backendVariants = await fetchVariants(appId) const variant = backendVariants[0] @@ -242,6 +156,26 @@ const TestsetTable: React.FC = ({mode}) => { } }, [writeMode, testset_id, appId]) + const handleExportClick = () => { + const csvData = convertToCsv( + rowData, + columnDefs.map((col) => col.field), + ) + const filename = `${testsetName}.csv` + downloadCsv(csvData, filename) + } + + const createNewColDefs = (colData: {field: string}[] = []) => { + return [ + CHECKBOX_COL, + ...colData.map((col) => ({ + ...col, + headerName: col.field, + })), + ADD_BUTTON_COL, + ] + } + const updateTable = (inputValues: string[]) => { const dataColumns = columnDefs.filter((colDef) => colDef.field !== "") @@ -251,14 +185,7 @@ const TestsetTable: React.FC = ({mode}) => { } }) - const newColumnDefs = [ - CHECKBOX_COL, - ...newDataColumns.map((col) => ({ - ...col, - headerName: col.field, - })), - ADD_BUTTON_COL, - ] + const newColumnDefs = createNewColDefs(newDataColumns) const keyMap = dataColumns.reduce((acc: KeyValuePair, colDef, index) => { acc[colDef.field] = newDataColumns[index].field @@ -281,151 +208,6 @@ const TestsetTable: React.FC = ({mode}) => { } } - const HeaderComponent = (params: any) => { - const {attributes} = params.eGridHeader - const [scopedInputValues, setScopedInputValues] = useState( - columnDefs.filter((colDef) => colDef.field !== "").map((col) => col.field), - ) - const index = attributes["aria-colindex"].nodeValue - 2 - const displayName = params.displayName - - const [isEditInputOpen, setIsEditInputOpen] = useState(false) - const handleOpenEditInput = () => { - setIsEditInputOpen(true) - } - - const handleSave = () => { - if (scopedInputValues[index] == inputValues[index]) { - setIsEditInputOpen(false) - - return - } - - if ( - inputValues.some((input) => input === scopedInputValues[index]) || - scopedInputValues[index] == "" - ) { - message.error( - scopedInputValues[index] == "" - ? "Invalid column name" - : "Column name already exist!", - ) - } else { - setInputValues(scopedInputValues) - updateTable(scopedInputValues) - setIsEditInputOpen(false) - } - } - - const handleInputChange = (index: number, event: any) => { - const values = [...inputValues] - values[index] = event.target.value - setScopedInputValues(values) - setLoading(false) - } - - const onAddColumn = () => { - const newColumnName = `column${columnDefs.length - 1}` - const newColmnDef = columnDefs - const updatedRowData = rowData.map((row) => ({ - ...row, - [newColumnName]: "", - })) - - newColmnDef.pop() - - setInputValues([...inputValues, newColumnName]) - setColumnDefs([ - ...columnDefs, - {field: newColumnName, headerName: newColumnName}, - ADD_BUTTON_COL, - ]) - setRowData(updatedRowData) - setLoading(false) - } - - useEffect(() => { - setScopedInputValues(inputValues) - }, [columnDefs]) - - useEffect(() => { - const handleEscape = (e: KeyboardEvent) => { - if (e.key == "Enter") { - if (isEditInputOpen) { - handleSave() - } - } - } - window.addEventListener("keydown", handleEscape) - return () => window.removeEventListener("keydown", handleEscape) - }, [isEditInputOpen, scopedInputValues]) - - if (displayName === "" && params.column?.colId !== "0") { - return ( -
- -
- ) - } else if (displayName === "" && params.column?.colId === "0") { - return - } else { - return ( - <> -
- {isEditInputOpen ? ( - handleInputChange(index, event)} - size="small" - /> - ) : ( - displayName - )} - -
- {isEditInputOpen ? ( -
-
- - ) - } - } - - const defaultColDef = { - flex: 1, - minWidth: 100, - editable: true, - cellRenderer: CellRenderer, - cellRendererParams: { - onEdit: (ix: number) => { - setFocusedRowData(rowData[ix]) - }, - }, - headerComponent: HeaderComponent, - resizable: true, - } - const onAddRow = () => { const newRow: KeyValuePair = {} columnDefs.forEach((colDef) => { @@ -434,7 +216,7 @@ const TestsetTable: React.FC = ({mode}) => { } }) setRowData([...rowData, newRow]) - setLoading(false) + setIsDataChanged(true) } const onSaveData = async () => { @@ -477,9 +259,9 @@ const TestsetTable: React.FC = ({mode}) => { } } - const handleChange = (e: React.ChangeEvent) => { - setTestsetName(e.target.value) - setLoading(false) + const onRowSelectedOrDeselected = () => { + if (!gridRef?.current) return + setSelectedRow(gridRef?.current?.getSelectedNodes()) } const onDeleteRow = () => { @@ -487,7 +269,7 @@ const TestsetTable: React.FC = ({mode}) => { const selectedData = selectedNodes.map((node: GenericObject) => node.data) const newrowData = rowData.filter((row) => !selectedData.includes(row)) setRowData(newrowData) - setLoading(false) + setIsDataChanged(true) } const onDeleteColumn = (indexToDelete: number) => { @@ -509,21 +291,51 @@ const TestsetTable: React.FC = ({mode}) => { setInputValues(newInputValues) setColumnDefs(newColumnDefs) setRowData(newRowData) - setLoading(false) + setIsDataChanged(true) if (gridRef.current) { gridRef.current.setColumnDefs(newColumnDefs) } } + const handleTestsetNameChange = (e: React.ChangeEvent) => { + setTestsetName(e.target.value) + setIsDataChanged(true) + } + const handleCellValueChanged = (params: GenericObject) => { if (params.newValue === null) { params.data[params.colDef.field] = "" } setUnSavedChanges(true) - setLoading(false) + setIsDataChanged(true) } - const {appTheme} = useAppTheme() + const defaultColDef = { + flex: 1, + minWidth: 100, + editable: true, + cellRenderer: TableCellsRenderer, + cellRendererParams: { + onEdit: (ix: number) => { + setFocusedRowData(rowData[ix]) + }, + }, + headerComponent: (params: IHeaderParams) => ( + + ), + resizable: true, + } return (
@@ -536,7 +348,7 @@ const TestsetTable: React.FC = ({mode}) => {
@@ -551,23 +363,14 @@ const TestsetTable: React.FC = ({mode}) => {
-
- Notes: -
-
- - - Specify column names similar to the Input parameters. - -
-
- - A column with - 'correct_answer' - - {" "} - name will be treated as a ground truth column and could be used in - evaluations. - -
+ Notes: + + - Specify column names similar to the Input parameters. + + + - A column with 'correct_answer' name will be treated as a + ground truth column and could be used in evaluations. +
= ({mode}) => { className="ph-no-capture" />
+ {selectedRow && (