Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions ami/main/api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ class ProjectViewSet(DefaultViewSet, ProjectMixin):
serializer_class = ProjectSerializer
pagination_class = ProjectPagination
permission_classes = [ObjectPermission]
ordering_fields = ["name", "created_at", "updated_at"]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if Project model's name field has a database index

rg -nP --type=py -C5 'class Project\(' --glob '**/models*.py' | rg -P '(db_index|Index|indexes)' -A10 -B10

Repository: RolnickLab/antenna

Length of output: 44


🏁 Script executed:

# Find Project model definition
rg -n 'class Project' --type=py

Repository: RolnickLab/antenna

Length of output: 1625


🏁 Script executed:

# Check if models files exist in ami/main
find ami/main -name '*models*' -type f

Repository: RolnickLab/antenna

Length of output: 81


🏁 Script executed:

# Get context around line 158 in views.py to understand the model being used
cat -n ami/main/api/views.py | sed -n '150,170p'

Repository: RolnickLab/antenna

Length of output: 1041


🏁 Script executed:

# Read the Project model definition
cat -n ami/main/models.py | sed -n '266,350p'

Repository: RolnickLab/antenna

Length of output: 3554


🏁 Script executed:

# Also search for Meta class and any indexes in Project model
rg -n 'class Project\(|name.*Field|db_index|class Meta' ami/main/models.py | head -50

Repository: RolnickLab/antenna

Length of output: 1559


🏁 Script executed:

# Read lines around 485 to check if Meta class is part of Project model
cat -n ami/main/models.py | sed -n '470,495p'

Repository: RolnickLab/antenna

Length of output: 1354


🏁 Script executed:

# Read the complete Meta class for Project model
cat -n ami/main/models.py | sed -n '485,550p'

Repository: RolnickLab/antenna

Length of output: 4529


🏁 Script executed:

# Continue reading the Meta class to see if indexes are defined
cat -n ami/main/models.py | sed -n '550,570p'

Repository: RolnickLab/antenna

Length of output: 770


🏁 Script executed:

# Final confirmation: search for any indexes definition in Project model
rg -n 'indexes\s*=' ami/main/models.py | head -20

Repository: RolnickLab/antenna

Length of output: 307


🏁 Script executed:

# Check which models have indexes defined (context around those lines)
rg -n -B10 'indexes\s*=' ami/main/models.py | grep -E '(^[0-9]+-class |indexes)' | head -30

Repository: RolnickLab/antenna

Length of output: 307


🏁 Script executed:

# Check if there are any migrations that add an index to Project.name
find . -name '*migrations*' -type d | head -5

Repository: RolnickLab/antenna

Length of output: 191


🏁 Script executed:

# Search for any migration files that might add index to name field
rg -n 'name.*index|AddIndex.*name' --type=py -i | grep -i project | head -20

Repository: RolnickLab/antenna

Length of output: 44


Add database index to Project.name field for sort performance.

The name field in the Project model currently lacks a database index, but is included in ordering_fields, enabling API sorting by project name. As the number of projects grows, queries sorted by name will scan the full table without an index. Add db_index=True to the name field in ami/main/models.py line 269 or define an index in the Project model's Meta class.

🧰 Tools
🪛 Ruff (0.15.12)

[warning] 158-158: Mutable default value for class attribute

(RUF012)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ami/main/api/views.py` at line 158, The Project model's name field is used in
ordering (ordering_fields includes "name") but lacks a DB index; update the
Project model by adding db_index=True to the name field declaration
(Project.name) or alternatively declare an Index on "name" inside the
Project.Meta.indexes so sorts by name use the index; migrate the DB after change
to apply the new index.


def get_queryset(self):
qs: ProjectQuerySet = super().get_queryset() # type: ignore
Expand Down
2 changes: 1 addition & 1 deletion ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"leaflet": "^1.9.3",
"lodash": "^4.17.21",
"lucide-react": "^1.0.1",
"nova-ui-kit": "^1.1.34",
"nova-ui-kit": "^1.1.36",
"plotly.js": "^2.25.2",
"react": "^18.2.0",
"react-dom": "^18.2.0",
Expand Down
98 changes: 69 additions & 29 deletions ui/src/design-system/components/sort-control.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import classNames from 'classnames'
import { ArrowUpDownIcon } from 'lucide-react'
import { buttonVariants, Select } from 'nova-ui-kit'
import { ArrowDownIcon, ArrowUpDownIcon } from 'lucide-react'
import { Button, buttonVariants, Select } from 'nova-ui-kit'
import { STRING, translate } from 'utils/language'
import { TableSortSettings } from './table/types'
import { BasicTooltip } from './tooltip/basic-tooltip'
Expand All @@ -16,33 +16,73 @@ export const SortControl = ({ columns, setSort, sort }: SortControlProps) => {
? columns.find((column) => column.sortField === sort.field)
: undefined

const changeSortField = (field: string) => {
if (sort) {
setSort({ field, order: sort.order })
} else {
setSort({ field, order: 'asc' })
}
}

const changeSortOrder = () => {
if (sort) {
setSort({
field: sort.field,
order: sort.order === 'asc' ? 'desc' : 'asc',
})
}
}

return (
<Select.Root
value={sort?.field}
onValueChange={(value) => {
setSort({ field: value, order: 'asc' })
}}
>
<BasicTooltip asChild content={translate(STRING.SORT_BY)}>
<Select.Trigger
className={classNames(
buttonVariants({ size: 'small', variant: 'outline' }),
'w-auto'
)}
>
<ArrowUpDownIcon className="w-4 h-4" />
<span>{column ? column.name : translate(STRING.SORT_BY)}</span>
</Select.Trigger>
</BasicTooltip>
<Select.Content>
{columns
.filter((column) => column.sortField)
.map((column) => (
<Select.Item key={column.id} value={column.sortField as string}>
{column.name}
</Select.Item>
))}
</Select.Content>
</Select.Root>
<div className="flex items-center gap-1">
<Select.Root value={sort?.field} onValueChange={changeSortField}>
<BasicTooltip asChild content={translate(STRING.SORT_BY)}>
<Select.Trigger
className={classNames(
buttonVariants({ size: 'small', variant: 'outline' }),
'w-auto'
)}
hideIcon={!!sort}
>
<span>{column ? column.name : translate(STRING.SORT_BY)}</span>
{sort ? (
<ArrowDownIcon
className={classNames(
'w-4 h-4 transition-transform duration-300',
{
'-rotate-180': sort.order !== 'asc',
'rotate-0': sort.order === 'asc',
}
)}
/>
) : null}
</Select.Trigger>
</BasicTooltip>
<Select.Content>
{columns
.filter((column) => column.sortField)
.map((column) => (
<Select.Item key={column.id} value={column.sortField as string}>
{column.name}
</Select.Item>
))}
</Select.Content>
</Select.Root>
{sort ? (
<BasicTooltip asChild content={translate(STRING.CHANGE_SORT_ORDER)}>
<Button onClick={changeSortOrder} size="icon" variant="ghost">
<ArrowUpDownIcon
className={classNames(
'w-4 h-4 transition-transform duration-300',
{
'-rotate-180': sort.order !== 'asc',
'rotate-0': sort.order === 'asc',
}
)}
/>
</Button>
</BasicTooltip>
) : null}
</div>
)
}
2 changes: 1 addition & 1 deletion ui/src/pages/jobs/jobs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ export const Jobs = () => {
title={translate(STRING.NAV_ITEM_JOBS)}
tooltip={translate(STRING.TOOLTIP_JOB)}
>
<SortControl columns={tableColumns} setSort={setSort} sort={sort} />
{canCreate ? <NewJobDialog /> : null}
<SortControl columns={tableColumns} setSort={setSort} sort={sort} />
<ColumnSettings
columns={tableColumns}
columnSettings={columnSettings}
Expand Down
2 changes: 1 addition & 1 deletion ui/src/pages/occurrences/occurrences.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,7 @@ export const Occurrences = () => {
to={APP_ROUTES.EXPORTS({ projectId: projectId as string })}
>
<DownloadIcon className="w-4 h-4" />
<span>Export </span>
<span>Export</span>
</Link>
<SortControl columns={tableColumns} setSort={setSort} sort={sort} />
<ColumnSettings
Expand Down
12 changes: 6 additions & 6 deletions ui/src/pages/project-details/new-project-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,20 @@ export const NewProjectDialog = ({
const { createProject, isLoading, isSuccess, error } = useCreateProject()
const [isOpen, setIsOpen] = useState(false)

const label = translate(STRING.ENTITY_CREATE, {
type: translate(STRING.ENTITY_TYPE_PROJECT),
})

return (
<Dialog.Root open={isOpen} onOpenChange={setIsOpen}>
<Dialog.Trigger asChild>
<Button size={buttonSize} variant={buttonVariant}>
<PlusIcon className="w-4 h-4" />
<span>{label}</span>
<span>{translate(STRING.CREATE_NEW)}</span>
</Button>
</Dialog.Trigger>
<Dialog.Content ariaCloselabel={translate(STRING.CLOSE)} isCompact>
<Dialog.Header title={label} />
<Dialog.Header
title={translate(STRING.ENTITY_CREATE, {
type: translate(STRING.ENTITY_TYPE_PROJECT),
})}
/>
<NewProjectForm
error={error}
isLoading={isLoading}
Expand Down
1 change: 0 additions & 1 deletion ui/src/pages/project/team/team-columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export const columns = ({
}): TableColumn<Member>[] => [
{
id: 'user',
sortField: 'name',
name: translate(STRING.FIELD_LABEL_USER),
renderCell: (item: Member) => (
<BasicTableCell>
Expand Down
2 changes: 1 addition & 1 deletion ui/src/pages/project/team/team.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const Team = () => {
}>()
const { pagination, setPage } = usePagination()
const { sort, setSort } = useSort({
field: 'name',
field: 'created_at',
order: 'asc',
})
const { members, userPermissions, total, isLoading, isFetching, error } =
Expand Down
2 changes: 0 additions & 2 deletions ui/src/pages/projects/project-gallery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ export const ProjectGallery = ({
return (
<Gallery
error={error}
cardSize={CardSize.Large}
isLoading={isLoading}
items={items}
renderItem={(item) => (
Expand All @@ -49,7 +48,6 @@ export const ProjectGallery = ({
/>
</Link>
)}
style={{ gridTemplateColumns: '1fr 1fr 1fr' }}
/>
)
}
25 changes: 19 additions & 6 deletions ui/src/pages/projects/projects.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useProjects } from 'data-services/hooks/projects/useProjects'
import { PageFooter } from 'design-system/components/page-footer/page-footer'
import { PageHeader } from 'design-system/components/page-header/page-header'
import { PaginationBar } from 'design-system/components/pagination-bar/pagination-bar'
import { SortControl } from 'design-system/components/sort-control'
import * as Tabs from 'design-system/components/tabs/tabs'
import { Button } from 'nova-ui-kit'
import { NewProjectDialog } from 'pages/project-details/new-project-dialog'
Expand All @@ -12,29 +13,33 @@ import { UserPermission } from 'utils/user/types'
import { useUser } from 'utils/user/userContext'
import { useUserInfo } from 'utils/user/userInfoContext'
import { useSelectedView } from 'utils/useSelectedView'
import { useWindowSize } from 'utils/useWindowSize'
import { useSort } from 'utils/useSort'
import { ProjectGallery } from './project-gallery'

export const TABS = {
MY_PROJECTS: 'my-projects',
ALL_PROJECTS: 'all-projects',
}

const SORT_FIELDS = [
{ id: 'name', name: translate(STRING.FIELD_LABEL_NAME) },
{ id: 'created_at', name: translate(STRING.FIELD_LABEL_CREATED_AT) },
{ id: 'updated_at', name: translate(STRING.FIELD_LABEL_UPDATED_AT) },
]

export const Projects = () => {
const { user } = useUser()
const { userInfo } = useUserInfo()
const { selectedView: selectedTab, setSelectedView: setSelectedTab } =
useSelectedView(user.loggedIn ? TABS.MY_PROJECTS : TABS.ALL_PROJECTS)
const [windowWidth] = useWindowSize()
const { pagination, setPage } = usePagination({
perPage: windowWidth > 1024 ? 21 : 20, // Adjust page size based on page width to avoid gallery gaps
})
const { sort, setSort } = useSort()
const { pagination, setPage } = usePagination({ perPage: 40 })
const filters =
user.loggedIn && selectedTab === TABS.MY_PROJECTS
? [{ field: 'user_id', value: userInfo?.id }]
: []
const { projects, total, userPermissions, isLoading, isFetching, error } =
useProjects({ pagination, filters })
useProjects({ pagination, filters, sort })
const canCreate = userPermissions?.includes(UserPermission.Create)

return (
Expand Down Expand Up @@ -67,6 +72,14 @@ export const Projects = () => {
</Tabs.Root>
) : null}
{canCreate ? <NewProjectDialog /> : null}
<SortControl
columns={SORT_FIELDS.map((field) => ({
...field,
sortField: field.id,
}))}
setSort={setSort}
sort={sort}
/>
</PageHeader>
{projects && projects.length === 0 && canCreate ? (
<div className="flex flex-col items-center pt-32">
Expand Down
4 changes: 4 additions & 0 deletions ui/src/utils/language.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ export enum STRING {
BACK,
CANCEL,
CHANGE_IMAGE,
CHANGE_SORT_ORDER,
CHOOSE_IMAGE,
CLEAR_FILTERS,
CLEAR,
COLLAPSE,
CONFIRM,
CONFIRMED,
CREATE_NEW,
CURRENT_LOCATION,
DELETE,
DELETED,
Expand Down Expand Up @@ -345,12 +347,14 @@ const ENGLISH_STRINGS: { [key in STRING]: string } = {
[STRING.BACK]: 'Back',
[STRING.CANCEL]: 'Cancel',
[STRING.CHANGE_IMAGE]: 'Change image',
[STRING.CHANGE_SORT_ORDER]: 'Change sort order',
[STRING.CHOOSE_IMAGE]: 'Choose image',
[STRING.CLEAR_FILTERS]: 'Clear filters',
[STRING.CLEAR]: 'Clear',
[STRING.COLLAPSE]: 'Collapse',
[STRING.CONFIRM]: 'Confirm',
[STRING.CONFIRMED]: 'Confirmed',
[STRING.CREATE_NEW]: 'Create new',
[STRING.CURRENT_LOCATION]: 'Use current location',
[STRING.DELETE]: 'Delete',
[STRING.DELETED]: 'Deleted',
Expand Down
10 changes: 5 additions & 5 deletions ui/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5002,7 +5002,7 @@ __metadata:
leaflet: "npm:^1.9.3"
lodash: "npm:^4.17.21"
lucide-react: "npm:^1.0.1"
nova-ui-kit: "npm:^1.1.34"
nova-ui-kit: "npm:^1.1.36"
plotly.js: "npm:^2.25.2"
postcss: "npm:^8.4.47"
prettier: "npm:2.8.4"
Expand Down Expand Up @@ -10562,9 +10562,9 @@ __metadata:
languageName: node
linkType: hard

"nova-ui-kit@npm:^1.1.34":
version: 1.1.34
resolution: "nova-ui-kit@npm:1.1.34"
"nova-ui-kit@npm:^1.1.36":
version: 1.1.36
resolution: "nova-ui-kit@npm:1.1.36"
dependencies:
"@radix-ui/react-checkbox": "npm:^1.1.4"
"@radix-ui/react-collapsible": "npm:^1.1.1"
Expand All @@ -10584,7 +10584,7 @@ __metadata:
react-dom: "npm:^18.3.1"
tailwind-merge: "npm:^2.5.4"
tailwindcss-animate: "npm:^1.0.7"
checksum: 4b3341ebebc715c79b5172870b30b33f65381bbf8d2acdcdda292e7be662592e32d188e9e33efd650c548211e45bef62efbb6af2ba464fb1eae37a31a02512db
checksum: c066ce1bc6d29339ee0596655135a9450bc55181eeb811970c93216889ef0361604897f3263905d2d0f01507f4296896111dff29d02770cd4da5f423aa46fef9
languageName: node
linkType: hard

Expand Down
Loading