Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(frontend): observability filters #2128

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
24a78ae
feat(frontend): implemented filters in observability
ashrafchowdury Oct 17, 2024
7e6a7c7
refactor(frontend): observability filter code
ashrafchowdury Oct 18, 2024
7978e69
feat(frontend): added nested filter on observability
ashrafchowdury Oct 20, 2024
d518ae0
fix(frontend): minor fixes
ashrafchowdury Oct 20, 2024
92b6ab4
fix(lint): error
ashrafchowdury Oct 21, 2024
723e45e
Merge branch 'AGE-950/-implement-drawer-for-observability-details' of…
ashrafchowdury Oct 21, 2024
e3737ad
fix(frontend): obserbility filter component logic
ashrafchowdury Oct 22, 2024
3ba157b
feat(frontend): added edit column and export data feature
ashrafchowdury Oct 22, 2024
54dab92
enhance(frontend): added search query filter
ashrafchowdury Oct 23, 2024
4196e19
Merge branch 'AGE-950/-implement-drawer-for-observability-details' of…
ashrafchowdury Oct 23, 2024
203d345
Merge branch 'AGE-950/-implement-drawer-for-observability-details' of…
ashrafchowdury Oct 23, 2024
8865f2d
Merge branch 'AGE-950/-implement-drawer-for-observability-details' of…
ashrafchowdury Oct 23, 2024
1bb42e2
Merge branch 'AGE-950/-implement-drawer-for-observability-details' of…
ashrafchowdury Oct 24, 2024
112e464
enhance(frontnd): made series of changes:
ashrafchowdury Oct 25, 2024
46c0c63
Merge branch 'AGE-950/-implement-drawer-for-observability-details' of…
ashrafchowdury Oct 25, 2024
577659f
fix(frontend): removed uncommit changes
ashrafchowdury Oct 25, 2024
2daaabe
Merge branch 'AGE-950/-implement-drawer-for-observability-details' of…
ashrafchowdury Oct 25, 2024
f14f850
temp(frontend): temporarily getting the project_id dynamically
ashrafchowdury Oct 25, 2024
b449fcb
Merge branch 'feature/observability-checkpoint-2' into AGE-1073/obser…
mmabrouk Oct 25, 2024
de4fefb
enhance(frontend): connect filters with backend apis
ashrafchowdury Oct 28, 2024
7827675
Merge branch 'AGE-950/-implement-drawer-for-observability-details' of…
ashrafchowdury Oct 28, 2024
d0870ca
refactor(frontend): cleaned up the components
ashrafchowdury Oct 28, 2024
13bc068
Merge branch 'feature/observability-checkpoint-2' of https://github.c…
ashrafchowdury Oct 28, 2024
01be3ac
feat(frontend): added pagination UI
ashrafchowdury Oct 28, 2024
aa1d542
enhane(frontend): refactor endpoint api url
ashrafchowdury Oct 28, 2024
147bf86
enhance(frontend): sort data functionalities
ashrafchowdury Oct 28, 2024
944d31a
Merge branch 'AGE-950/-implement-drawer-for-observability-details' of…
ashrafchowdury Oct 28, 2024
57fbf23
enhance(frontend): synced all quaries with a single endpoint
ashrafchowdury Oct 28, 2024
aa1af02
Merge branch 'AGE-950/-implement-drawer-for-observability-details' in…
bekossy Oct 29, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions agenta-web/src/components/Filters/EditColumns.tsx
Original file line number Diff line number Diff line change
@@ -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<any>
onChange: (key: string) => void
excludes?: string[] // Array of column keys to exclude
buttonText?: string
}

const EditColumns: React.FC<EditColumnsProps> = ({
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: (
<Space onClick={(e) => e.stopPropagation()}>
<Checkbox
value={col.key}
checked={!selectedKeys.includes(col.key as string)}
onChange={() => onChange(col.key as string)}
/>
{col.title as string}
</Space>
),
},
...(("children" in col &&
col.children?.map((child) => ({
key: child.key as React.Key,
label: (
<Space className="ml-4" onClick={(e) => e.stopPropagation()}>
<Checkbox
value={child.key}
checked={!selectedKeys.includes(child.key as string)}
onChange={() => onChange(child.key as string)}
/>
{child.title as string}
</Space>
),
}))) ||
[]),
])
}

return (
<Dropdown
trigger={["click"]}
open={open}
onOpenChange={handleDropdownChange}
menu={{
selectedKeys,
items: generateEditItems(),
className: classes.dropdownMenu,
}}
>
<Button icon={<Columns size={14} />} className={classes.button}>
{buttonText}
</Button>
</Dropdown>
)
}

export default EditColumns
244 changes: 244 additions & 0 deletions agenta-web/src/components/Filters/Filters.tsx
Original file line number Diff line number Diff line change
@@ -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<Props> = ({filterData, columns, onApplyFilter, onClearFilter}) => {
const classes = useStyles()
const emptyFilter = [{key: "", operator: "", value: ""}] as Filter[]

const [filter, setFilter] = useState<Filter[]>(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 (
<Popover
title={null}
trigger="click"
overlayClassName={classes.popover}
arrow={false}
onOpenChange={() => setIsFilterOpen(false)}
open={isFilterOpen}
placement="bottomLeft"
content={
<section>
<div className="h-[44px] flex items-center justify-between">
<Typography.Text className={classes.filterHeading}>Filter</Typography.Text>
</div>

<div className="-ml-4 -mr-2">
<Divider className="!m-0" />
</div>

<div className={classes.filterContainer}>
{filter.map((item, idx) => (
<Space key={idx}>
<p className={`w-[60px] text-end`}>{idx == 0 ? "Where" : "And"}</p>

<Select
showSearch
labelRender={(label) => (!label.value ? "Column" : label.label)}
style={{width: 100}}
popupMatchSelectWidth={220}
suffixIcon={<CaretDown size={14} />}
onChange={(value) =>
onFilterChange({columnName: "key", value, idx})
}
filterSort={(a, b) =>
(a?.label ?? "")
.toLowerCase()
.localeCompare((b?.label ?? "").toLowerCase())
}
filterOption={(input, option) =>
(option?.label ?? "")
.toLowerCase()
.includes(input.toLowerCase())
}
value={item.key}
options={filteredOptions.map((col) => ({
value: col.value,
label: col.label,
}))}
/>
{item.key && (
<>
<Select
labelRender={(label) =>
!label.value ? "Condition" : label.label
}
style={{width: 95}}
suffixIcon={<CaretDown size={14} />}
onChange={(value) =>
onFilterChange({
columnName: "operator",
value,
idx,
})
}
popupMatchSelectWidth={100}
value={item.operator}
options={operators.map((operator) => ({
value: operator.value,
label: operator.lable,
}))}
/>

<Input
placeholder="Keyword"
className="w-[270px]"
value={item.value}
onChange={(e) =>
onFilterChange({
columnName: "value",
value: e.target.value,
idx,
})
}
/>
</>
)}
{filter.length > 1 && (
<Button
type="link"
icon={<Trash size={14} />}
onClick={() => onDeleteFilter(idx)}
/>
)}
</Space>
))}

<Button
type="link"
size="small"
icon={<Plus size={14} />}
onClick={addNestedFilter}
className="mt-2"
>
Add conditions
</Button>
</div>

<Space className="flex items-center justify-end mt-2">
<Button type="link">Cancel</Button>
<Button
icon={<ArrowCounterClockwise size={14} className="mt-0.5" />}
onClick={clearFilter}
>
Clear
</Button>
<Button
type="primary"
disabled={!filter[0]?.operator}
onClick={applyFilter}
>
Apply
</Button>
</Space>
</section>
}
>
<Button
icon={<Funnel size={14} />}
onClick={() => setIsFilterOpen(true)}
className="flex items-center gap-2"
>
Filters {filter[0]?.operator && <X size={14} />}
</Button>
</Popover>
)
}

export default Filters
Loading