diff --git a/agenta-web/src/components/Filters/EditColumns.tsx b/agenta-web/src/components/Filters/EditColumns.tsx new file mode 100644 index 0000000000..2e4eb894e6 --- /dev/null +++ b/agenta-web/src/components/Filters/EditColumns.tsx @@ -0,0 +1,110 @@ +import React, {useState} from "react" +import {Button, Dropdown, Space, Checkbox} from "antd" +import {createUseStyles} from "react-jss" +import {Columns} from "@phosphor-icons/react" +import {ColumnsType} from "antd/es/table" +import type {MenuProps} from "antd" + +const useStyles = createUseStyles((theme) => ({ + dropdownMenu: { + "&>.ant-dropdown-menu-item": { + "& .anticon-check": { + display: "none", + }, + }, + "&>.ant-dropdown-menu-item-selected": { + "&:not(:hover)": { + backgroundColor: "transparent !important", + }, + "& .anticon-check": { + display: "inline-flex !important", + }, + }, + }, + button: { + display: "flex", + alignItems: "center", + }, +})) + +interface EditColumnsProps { + isOpen: boolean + handleOpenChange: (open: boolean) => void + selectedKeys: string[] + columns: ColumnsType + onChange: (key: string) => void + excludes?: string[] // Array of column keys to exclude + buttonText?: string +} + +const EditColumns: React.FC = ({ + isOpen, + handleOpenChange, + selectedKeys, + columns, + onChange, + excludes = [], + buttonText = "Edit Columns", +}) => { + const classes = useStyles() + const [open, setOpen] = useState(isOpen) + + const handleDropdownChange = (newOpen: boolean) => { + setOpen(newOpen) + if (!newOpen) handleOpenChange(newOpen) + } + + const generateEditItems = (): MenuProps["items"] => { + return columns + .filter((col) => !excludes.includes(col.key as string)) + .flatMap((col) => [ + { + key: col.key as React.Key, + label: ( + e.stopPropagation()}> + onChange(col.key as string)} + /> + {col.title as string} + + ), + }, + ...(("children" in col && + col.children?.map((child) => ({ + key: child.key as React.Key, + label: ( + e.stopPropagation()}> + onChange(child.key as string)} + /> + {child.title as string} + + ), + }))) || + []), + ]) + } + + return ( + + + + ) +} + +export default EditColumns diff --git a/agenta-web/src/components/Filters/Filters.tsx b/agenta-web/src/components/Filters/Filters.tsx new file mode 100644 index 0000000000..f48313cc5d --- /dev/null +++ b/agenta-web/src/components/Filters/Filters.tsx @@ -0,0 +1,244 @@ +import React, {useState} from "react" +import {Filter, JSSTheme} from "@/lib/Types" +import {ArrowCounterClockwise, CaretDown, Funnel, Plus, Trash, X} from "@phosphor-icons/react" +import {Button, Divider, Input, Popover, Select, Space, Typography} from "antd" +import {createUseStyles} from "react-jss" +import {useUpdateEffect} from "usehooks-ts" + +const useStyles = createUseStyles((theme: JSSTheme) => ({ + popover: { + "& .ant-popover-inner": { + width: "600px !important", + padding: `0px ${theme.paddingXS}px ${theme.paddingXS}px ${theme.padding}px`, + }, + }, + filterHeading: { + fontSize: theme.fontSizeHeading5, + lineHeight: theme.lineHeightHeading5, + fontWeight: theme.fontWeightMedium, + }, + filterContainer: { + padding: 8, + display: "flex", + flexDirection: "column", + alignItems: "start", + borderRadius: theme.borderRadius, + backgroundColor: "#f5f7fa", + marginTop: 8, + }, +})) + +type Props = { + filterData?: Filter[] + columns: {value: string; label: string}[] + onApplyFilter: (filters: Filter[]) => void + onClearFilter: (filters: Filter[]) => void +} + +const Filters: React.FC = ({filterData, columns, onApplyFilter, onClearFilter}) => { + const classes = useStyles() + const emptyFilter = [{key: "", operator: "", value: ""}] as Filter[] + + const [filter, setFilter] = useState(emptyFilter) + const [isFilterOpen, setIsFilterOpen] = useState(false) + + useUpdateEffect(() => { + if (filterData && filterData.length > 0) { + setFilter(filterData) + } else { + setFilter(emptyFilter) + } + }, [filterData]) + + const operators = [ + {value: "contains", lable: "contains"}, + {value: "matches", lable: "matches"}, + {value: "like", lable: "like"}, + {value: "startswith", lable: "startswith"}, + {value: "endswith", lable: "endswith"}, + {value: "exists", lable: "exists"}, + {value: "not_exists", lable: "not exists"}, + {value: "eq", lable: "="}, + {value: "neq", lable: "!="}, + {value: "gt", lable: ">"}, + {value: "lt", lable: "<"}, + {value: "gte", lable: ">="}, + {value: "lte", lable: "<="}, + ] + + const filteredOptions = columns.filter( + (col) => !filter.some((item, i) => item.key === col.value), + ) + + const onFilterChange = ({ + columnName, + value, + idx, + }: { + columnName: keyof Filter + value: any + idx: number + }) => { + const newFilters = [...filter] + newFilters[idx][columnName as keyof Filter] = value + setFilter(newFilters) + } + + const onDeleteFilter = (index: number) => { + setFilter(filter.filter((_, idx) => idx !== index)) + } + + const addNestedFilter = () => { + setFilter([...filter, {key: "", operator: "", value: ""}]) + } + + const clearFilter = () => { + setFilter(emptyFilter) + onClearFilter(emptyFilter) + } + + const applyFilter = () => { + const sanitizedFilters = filter.filter(({key, operator}) => key && operator) + + onApplyFilter(sanitizedFilters) + setIsFilterOpen(false) + } + + return ( + setIsFilterOpen(false)} + open={isFilterOpen} + placement="bottomLeft" + content={ +
+
+ Filter +
+ +
+ +
+ +
+ {filter.map((item, idx) => ( + +

{idx == 0 ? "Where" : "And"}

+ + + !label.value ? "Condition" : label.label + } + style={{width: 95}} + suffixIcon={} + onChange={(value) => + onFilterChange({ + columnName: "operator", + value, + idx, + }) + } + popupMatchSelectWidth={100} + value={item.operator} + options={operators.map((operator) => ({ + value: operator.value, + label: operator.lable, + }))} + /> + + + onFilterChange({ + columnName: "value", + value: e.target.value, + idx, + }) + } + /> + + )} + {filter.length > 1 && ( + +
+ + + + + + +
+ } + > + +
+ ) +} + +export default Filters diff --git a/agenta-web/src/components/Filters/Sort.tsx b/agenta-web/src/components/Filters/Sort.tsx new file mode 100644 index 0000000000..9dee076576 --- /dev/null +++ b/agenta-web/src/components/Filters/Sort.tsx @@ -0,0 +1,286 @@ +import React, {useState} from "react" +import {CaretRight, Clock, Calendar} from "@phosphor-icons/react" +import {DatePicker, Button, Typography, Divider, Popover} from "antd" +import {JSSTheme, SortTypes} from "@/lib/Types" +import dayjs, {Dayjs} from "dayjs" +import type {SelectProps} from "antd" +import {createUseStyles} from "react-jss" + +const useStyles = createUseStyles((theme: JSSTheme) => ({ + title: { + fontSize: theme.fontSizeLG, + fontWeight: theme.fontWeightMedium, + padding: theme.paddingXS, + }, + customDateContainer: { + flex: 1, + padding: theme.paddingXS, + gap: 16, + display: "flex", + flexDirection: "column", + }, + popover: { + "& .ant-popover-inner": { + transition: "width 0.3s ease", + padding: 4, + }, + }, + popupItems: { + display: "flex", + alignItems: "center", + justifyContent: "space-between", + padding: `5px ${theme.paddingContentHorizontal}px`, + gap: theme.marginXS, + borderRadius: theme.borderRadiusSM, + cursor: "pointer", + "&:hover": { + backgroundColor: theme.controlItemBgActive, + }, + }, + popupSelectedItem: { + backgroundColor: theme.controlItemBgActive, + }, +})) + +export type SortResult = { + type: "custom" | "standerd" + sorted: string + customRange?: {startTime: string; endTime: string} +} + +type Props = { + onSortApply: ({ + type, + sorted, + customRange, + }: { + type: "custom" | "standerd" + sorted: string + customRange?: {startTime: string; endTime: string} + }) => void + defaultSortValue: SortTypes +} +export type CustomTimeRange = { + startTime: Dayjs | null + endTime: Dayjs | null +} + +const Sort: React.FC = ({onSortApply, defaultSortValue}) => { + const classes = useStyles() + + const [sort, setSort] = useState(defaultSortValue) + const [customTime, setCustomTime] = useState({startTime: null, endTime: null}) + const [dropdownVisible, setDropdownVisible] = useState(false) + const [customOptionSelected, setCustomOptionSelected] = useState( + customTime.startTime == null ? false : true, + ) + + const apply = ({ + sortData, + customRange, + }: { + sortData: SortTypes + customRange?: CustomTimeRange + }) => { + let sortedTime + let customRangeTime + + if (sortData && sortData !== "custom" && sortData !== "all time") { + const now = dayjs().utc() + + // Split the value into number and unit (e.g., "30 minutes" becomes ["30", "minutes"]) + const [amount, unit] = (sortData as SortTypes).split(" ") + sortedTime = now + .subtract(parseInt(amount), unit as dayjs.ManipulateType) + .toISOString() + .split(".")[0] + } else if (customRange?.startTime && sortData == "custom") { + customRangeTime = { + startTime: customRange.startTime.toISOString().split(".")[0], + endTime: customRange.endTime?.toISOString().split(".")[0] as string, + } + } else if (sortData === "all time") { + sortedTime = "1970-01-01T00:00:00" + } + + onSortApply({ + type: sortData == "custom" ? "custom" : "standerd", + sorted: sortedTime as string, + customRange: customRangeTime, + }) + } + + const handleApplyCustomRange = () => { + if (customTime.startTime && customTime.endTime) { + apply({sortData: "custom", customRange: customTime}) + setDropdownVisible(false) + } + } + + const onSelectItem = (item: any) => { + setDropdownVisible(false) + setSort(item.value as SortTypes) + apply({sortData: item.value as SortTypes}) + + setTimeout(() => { + setCustomOptionSelected(false) + }, 500) + + if (customTime.startTime) { + setCustomTime({startTime: null, endTime: null}) + } + } + + const options: SelectProps["options"] = [ + {value: "30 minutes", label: "30 mins"}, + {value: "1 hour", label: "1 hour"}, + {value: "6 hours", label: "6 hours"}, + {value: "24 hours", label: "24 hours"}, + {value: "3 days", label: "3 days"}, + {value: "7 days", label: "7 days"}, + {value: "14 days", label: "14 days"}, + {value: "1 month", label: "1 month"}, + {value: "3 months", label: "3 months"}, + {value: "all time", label: "All time"}, + ] + + return ( + <> + { + if (sort == "custom" && customTime.startTime == null) { + setSort(defaultSortValue) + setCustomOptionSelected(false) + } + }} + onOpenChange={() => setDropdownVisible(false)} + open={dropdownVisible} + placement="bottomLeft" + content={ +
+
+
+ {options.map((item) => ( +
onSelectItem(item)} + className={`${classes.popupItems} ${sort === item.value && classes.popupSelectedItem}`} + > + {item.label} +
+ ))} + +
+ +
+ +
{ + setCustomOptionSelected(true) + setSort("custom") + }} + > + + Define start and end time + + +
+
+
+ + {customOptionSelected && ( + <> + + +
+
+ + Start and end time + + +
+
+ Start time + + setCustomTime({ + ...customTime, + startTime: date, + }) + } + style={{width: "100%"}} + /> +
+ +
+ End time + + setCustomTime({ + ...customTime, + endTime: date, + }) + } + style={{width: "100%"}} + /> +
+
+
+ +
+ + +
+
+ + )} +
+ } + > + +
+ + ) +} + +export default Sort diff --git a/agenta-web/src/components/pages/observability/drawer/TraceContent.tsx b/agenta-web/src/components/pages/observability/drawer/TraceContent.tsx index daafc29f26..5e383af457 100644 --- a/agenta-web/src/components/pages/observability/drawer/TraceContent.tsx +++ b/agenta-web/src/components/pages/observability/drawer/TraceContent.tsx @@ -222,10 +222,10 @@ const TraceContent = ({activeTrace}: TraceContentProps) => { - {!activeTrace.parent && activeTrace.refs?.application.id && ( + {!activeTrace.parent && activeTrace.refs?.application_id && (