Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
6 changes: 4 additions & 2 deletions ami/main/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,14 @@ def get_media_url(path: str) -> str:

def get_or_create_default_device(project: "Project") -> "Device":
"""Create a default device for a project."""
device, _created = Device.objects.get_or_create(name="Default device", project=project)
device, _created = Device.objects.get_or_create(name="Default Device", project=project)
Copy link
Collaborator

Choose a reason for hiding this comment

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

I thought you preferred the other case!

Copy link
Member Author

Choose a reason for hiding this comment

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

Haha I have no preference really, I just want things to be consistent. I noticed that for auto created entities, it seems we often use Capitalized Case (Default Station, All Images, etc.).

logger.info(f"Created default device for project {project}")
return device


def get_or_create_default_research_site(project: "Project") -> "Site":
"""Create a default research site for a project."""
site, _created = Site.objects.get_or_create(name="Default site", project=project)
site, _created = Site.objects.get_or_create(name="Default Site", project=project)
logger.info(f"Created default research site for project {project}")
return site

Expand All @@ -119,6 +119,8 @@ def get_or_create_default_deployment(
project=project,
research_site=site,
device=device,
latitude=0,
Copy link
Member Author

Choose a reason for hiding this comment

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

This is because I noticed the default project didn't pass validation and couldn't be saved.

longitude=0,
)
logger.info(f"Created default deployment for project {project}")
return deployment
Expand Down
4 changes: 2 additions & 2 deletions ui/src/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { Auth } from 'pages/auth/auth'
import { Login } from 'pages/auth/login'
import { ResetPassword } from 'pages/auth/reset-password'
import { ResetPasswordConfirm } from 'pages/auth/reset-password-confirm'
import { CollectionDetails } from 'pages/collection-details/collection-details'
import { Captures } from 'pages/captures/captures'
import { Deployments } from 'pages/deployments/deployments'
import { Jobs } from 'pages/jobs/jobs'
import { Occurrences } from 'pages/occurrences/occurrences'
Expand Down Expand Up @@ -108,7 +108,6 @@ export const App = () => (
/>
<Route path="summary" element={<Summary />} />
<Route path="collections" element={<Collections />} />
<Route path="collections/:id" element={<CollectionDetails />} />
Copy link
Member Author

Choose a reason for hiding this comment

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

Setup redirect route?

<Route path="exports/:id?" element={<Exports />} />
<Route
path="processing-services/:id?"
Expand All @@ -124,6 +123,7 @@ export const App = () => (
</Route>
<Route path="jobs/:id?" element={<Jobs />} />
<Route path="deployments/:id?" element={<Deployments />} />
<Route path="captures" element={<Captures />} />
<Route path="sessions" element={<Sessions />} />
<Route path="sessions/:id" element={<SessionDetails />} />
<Route path="occurrences/:id?" element={<Occurrences />} />
Expand Down
1 change: 1 addition & 0 deletions ui/src/components/filtering/filter-control.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ const ComponentMap: {
best_determination_score: ScoreFilter,
classification_threshold: ScoreFilter,
collection: CollectionFilter,
collections: CollectionFilter,
date_end: DateFilter,
date_start: DateFilter,
deployment: StationFilter,
Expand Down
4 changes: 4 additions & 0 deletions ui/src/data-services/hooks/captures/useCaptures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Capture, ServerCapture } from 'data-services/models/capture'
import { FetchParams } from 'data-services/types'
import { getFetchUrl } from 'data-services/utils'
import { useMemo } from 'react'
import { UserPermission } from 'utils/user/types'
import { useAuthorizedQuery } from '../auth/useAuthorizedQuery'

const convertServerRecord = (record: ServerCapture) => new Capture(record)
Expand All @@ -11,6 +12,7 @@ export const useCaptures = (
params: FetchParams
): {
captures?: Capture[]
userPermissions?: UserPermission[]
total: number
isLoading: boolean
isFetching: boolean
Expand All @@ -20,6 +22,7 @@ export const useCaptures = (

const { data, isLoading, isFetching, error } = useAuthorizedQuery<{
results: ServerCapture[]
user_permissions?: UserPermission[]
count: number
}>({
queryKey: [API_ROUTES.CAPTURES, params],
Expand All @@ -31,6 +34,7 @@ export const useCaptures = (

return {
captures,
userPermissions: data?.user_permissions,
total: data?.count ?? 0,
isLoading,
isFetching,
Expand Down
6 changes: 4 additions & 2 deletions ui/src/data-services/hooks/captures/useUploadCapture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export const useUploadCapture = (onSuccess?: (id: string) => void) => {
const { user } = useUser()
const queryClient = useQueryClient()

const { mutate, isLoading, error, isSuccess } = useMutation({
const { mutateAsync, isLoading, error, isSuccess, reset } = useMutation({
mutationFn: (fieldValues: UploadCaptureFieldValues) => {
const data = new FormData()
if (fieldValues.projectId) {
Expand All @@ -41,10 +41,12 @@ export const useUploadCapture = (onSuccess?: (id: string) => void) => {
)
},
onSuccess: ({ data }) => {
queryClient.invalidateQueries([API_ROUTES.CAPTURES])
queryClient.invalidateQueries([API_ROUTES.DEPLOYMENTS])
queryClient.invalidateQueries([API_ROUTES.SUMMARY])
onSuccess?.(`${data.source_image.id}`)
},
})

return { uploadCapture: mutate, isLoading, error, isSuccess }
return { uploadCapture: mutateAsync, isLoading, error, isSuccess, reset }
}
64 changes: 64 additions & 0 deletions ui/src/data-services/hooks/captures/useUploadCaptures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import { useQueryClient } from '@tanstack/react-query'
import { API_ROUTES, SUCCESS_TIMEOUT } from 'data-services/constants'
import { useState } from 'react'
import { useUploadCapture } from './useUploadCapture'

type Result = PromiseSettledResult<any>

const isRejected = (result: Result) => result.status === 'rejected'

export const useUploadCaptures = (onSuccess?: () => void) => {
const queryClient = useQueryClient()
const [results, setResults] = useState<Result[]>()
const { uploadCapture, isLoading, isSuccess, reset } = useUploadCapture(
() => {
setTimeout(() => {
reset()
}, SUCCESS_TIMEOUT)
}
)

const error = results?.some(isRejected)
? 'Not all images could not be uploaded, please retry.'
: undefined

return {
isLoading,
isSuccess,
error,
uploadCaptures: async (params: {
projectId: string
deploymentId: string
files: File[]
}) => {
const promises = params.files
.filter((_, index) => {
if (error) {
// Only retry rejected requests
return results?.[index]?.status === 'rejected'
}

return true
})
.map((file) =>
uploadCapture({
projectId: params.projectId,
deploymentId: params.deploymentId,
file,
})
)

setResults(undefined)
const updatesResults = await Promise.allSettled(promises)
setResults(updatesResults)

if (!updatesResults?.some(isRejected)) {
queryClient.invalidateQueries([API_ROUTES.CAPTURES])
queryClient.invalidateQueries([API_ROUTES.DEPLOYMENTS])
queryClient.invalidateQueries([API_ROUTES.SUMMARY])
queryClient.invalidateQueries([API_ROUTES.PROJECTS, params.projectId])
onSuccess?.()
}
},
}
}
2 changes: 1 addition & 1 deletion ui/src/data-services/hooks/projects/useCreateProject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const useCreateProject = (onSuccess?: () => void) => {

const { mutateAsync, isLoading, isSuccess, reset, error } = useMutation({
mutationFn: (fieldValues: any) =>
axios.post(
axios.post<{ id: number }>(
`${API_URL}/${API_ROUTES.PROJECTS}/`,
convertToServerFormData(fieldValues),
{
Expand Down
3 changes: 2 additions & 1 deletion ui/src/data-services/models/collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ export class Collection extends Entity {
this.numImagesWithDetections && this.numImages
? (this.numImagesWithDetections / this.numImages) * 100
: 0
return `${this.numImagesWithDetections?.toLocaleString()} / ${this.numImages?.toLocaleString()} (${pct.toFixed(

return `${this.numImagesWithDetections?.toLocaleString()} (${pct.toFixed(
0
)}%)`
}
Expand Down
5 changes: 5 additions & 0 deletions ui/src/data-services/models/project-details.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { UserPermission } from 'utils/user/types'
import { Project } from './project'

export type ServerProject = any // TODO: Update this type
Expand Down Expand Up @@ -51,4 +52,8 @@ export class ProjectDetails extends Project {
get summaryData(): SummaryData[] {
return this._project.summary_data
}

get userPermissions(): UserPermission[] {
return this._project.user_permissions
}
}
3 changes: 1 addition & 2 deletions ui/src/design-system/components/dialog/dialog.module.scss
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,7 @@ $dialog-padding-medium: 32px;
.dialogTitle {
all: unset;
display: block;
@include paragraph-large();
font-weight: 600;
@include heading-6();
color: $color-neutral-700;
padding-top: 2px;
}
Expand Down
2 changes: 1 addition & 1 deletion ui/src/design-system/components/select/select.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export const Select = ({
label,
loading,
options = [],
placeholder = 'Pick a value',
placeholder = 'Select a value',
showClear = true,
theme = SelectTheme.Default,
value,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { FilterControl } from 'components/filtering/filter-control'
import { FilterSection } from 'components/filtering/filter-section'
import { useCaptures } from 'data-services/hooks/captures/useCaptures'
import { useCollectionDetails } from 'data-services/hooks/collections/useCollectionDetails'
import { IconType } from 'design-system/components/icon/icon'
import { PageFooter } from 'design-system/components/page-footer/page-footer'
import { PageHeader } from 'design-system/components/page-header/page-header'
Expand All @@ -10,40 +11,42 @@ import { ToggleGroup } from 'design-system/components/toggle-group/toggle-group'
import { useState } from 'react'
import { useParams } from 'react-router-dom'
import { STRING, translate } from 'utils/language'
import { useFilters } from 'utils/useFilters'
import { usePagination } from 'utils/usePagination'
import { UserPermission } from 'utils/user/types'
import { useSelectedView } from 'utils/useSelectedView'
import { columns } from './capture-columns'
import { CaptureGallery } from './capture-gallery'
import { UploadImagesDialog } from './upload-images-dialog/upload-images-dialog'

export const CollectionDetails = () => {
const { projectId, id } = useParams()
export const Captures = () => {
const { projectId } = useParams()
const { selectedView, setSelectedView } = useSelectedView('table')

// Collection details
const { collection } = useCollectionDetails(id as string)

// Collection captures
const { filters } = useFilters()
const [sort, setSort] = useState<TableSortSettings>()
const { pagination, setPage } = usePagination()
const { captures, total, isLoading, isFetching, error } = useCaptures({
projectId,
pagination,
sort,
filters: [
{
field: 'collections',
value: id as string,
},
],
})
const { captures, userPermissions, total, isLoading, isFetching, error } =
useCaptures({
projectId,
sort,
pagination,
filters,
})
const canCreate = userPermissions?.includes(UserPermission.Create)

return (
<>
{collection && (
<div className="flex flex-col gap-6 md:flex-row">
<div className="space-y-6">
<FilterSection defaultOpen>
<FilterControl field="deployment" />
<FilterControl field="collections" />
</FilterSection>
</div>
<div className="w-full overflow-hidden">
<PageHeader
title={collection.name}
title={translate(STRING.NAV_ITEM_CAPTURES)}
subTitle={translate(STRING.RESULTS, {
total,
total: captures?.length ?? 0,
})}
isLoading={isLoading}
isFetching={isFetching}
Expand All @@ -64,26 +67,27 @@ export const CollectionDetails = () => {
value={selectedView}
onValueChange={setSelectedView}
/>
{canCreate ? <UploadImagesDialog /> : null}
</PageHeader>
)}
{selectedView === 'table' && (
<Table
columns={columns(projectId as string)}
error={error}
isLoading={isLoading}
items={captures}
onSortSettingsChange={setSort}
sortable
sortSettings={sort}
/>
)}
{selectedView === 'gallery' && (
<CaptureGallery
captures={captures}
error={error}
isLoading={isLoading}
/>
)}
{selectedView === 'table' && (
<Table
columns={columns(projectId as string)}
error={error}
isLoading={isLoading}
items={captures}
onSortSettingsChange={setSort}
sortable
sortSettings={sort}
/>
)}
{selectedView === 'gallery' && (
<CaptureGallery
captures={captures}
error={error}
isLoading={isLoading}
/>
)}
</div>
<PageFooter>
{captures?.length ? (
<PaginationBar
Expand All @@ -93,6 +97,6 @@ export const CollectionDetails = () => {
/>
) : null}
</PageFooter>
</>
</div>
)
}
Loading