Skip to content

Commit 8cd2c9c

Browse files
annavikmihow
andauthored
Make it easier for users to upload and explore captures (#943)
* fix: make sure default station passes validation * feat: setup dedicated capture view * copy: streamline casing for default entities * feat: setup and use image upload dialog * feat: show get started information in project gallery * copy: streamline select placeholders * feat: navigate to project after creation * fix: tweak capture links * fix: tweak default column settings for collections * copy: tweak wording * fix: bring back jobs as main menu item * fix: link to session detail * fix: missing occurrence filter for single source images --------- Co-authored-by: Michael Bunsen <notbot@gmail.com>
1 parent c834d98 commit 8cd2c9c

33 files changed

+727
-133
lines changed

ami/main/api/views.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1080,6 +1080,7 @@ class OccurrenceViewSet(DefaultViewSet, ProjectMixin):
10801080
"event",
10811081
"deployment",
10821082
"determination__rank",
1083+
"detections__source_image",
10831084
]
10841085
ordering_fields = [
10851086
"created_at",

ami/main/models.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,14 +98,14 @@ def get_media_url(path: str) -> str:
9898

9999
def get_or_create_default_device(project: "Project") -> "Device":
100100
"""Create a default device for a project."""
101-
device, _created = Device.objects.get_or_create(name="Default device", project=project)
101+
device, _created = Device.objects.get_or_create(name="Default Device", project=project)
102102
logger.info(f"Created default device for project {project}")
103103
return device
104104

105105

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

@@ -119,6 +119,8 @@ def get_or_create_default_deployment(
119119
project=project,
120120
research_site=site,
121121
device=device,
122+
latitude=0,
123+
longitude=0,
122124
)
123125
logger.info(f"Created default deployment for project {project}")
124126
return deployment

ui/src/app.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { Auth } from 'pages/auth/auth'
1717
import { Login } from 'pages/auth/login'
1818
import { ResetPassword } from 'pages/auth/reset-password'
1919
import { ResetPasswordConfirm } from 'pages/auth/reset-password-confirm'
20-
import { CollectionDetails } from 'pages/collection-details/collection-details'
20+
import { Captures } from 'pages/captures/captures'
2121
import { Deployments } from 'pages/deployments/deployments'
2222
import { Jobs } from 'pages/jobs/jobs'
2323
import { Occurrences } from 'pages/occurrences/occurrences'
@@ -108,7 +108,6 @@ export const App = () => (
108108
/>
109109
<Route path="summary" element={<Summary />} />
110110
<Route path="collections" element={<Collections />} />
111-
<Route path="collections/:id" element={<CollectionDetails />} />
112111
<Route path="exports/:id?" element={<Exports />} />
113112
<Route
114113
path="processing-services/:id?"
@@ -124,6 +123,7 @@ export const App = () => (
124123
</Route>
125124
<Route path="jobs/:id?" element={<Jobs />} />
126125
<Route path="deployments/:id?" element={<Deployments />} />
126+
<Route path="captures" element={<Captures />} />
127127
<Route path="sessions" element={<Sessions />} />
128128
<Route path="sessions/:id" element={<SessionDetails />} />
129129
<Route path="occurrences/:id?" element={<Occurrences />} />

ui/src/components/filtering/filter-control.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ const ComponentMap: {
2626
best_determination_score: ScoreFilter,
2727
classification_threshold: ScoreFilter,
2828
collection: CollectionFilter,
29+
collections: CollectionFilter,
2930
date_end: DateFilter,
3031
date_start: DateFilter,
3132
deployment: StationFilter,

ui/src/data-services/hooks/captures/useCaptures.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Capture, ServerCapture } from 'data-services/models/capture'
33
import { FetchParams } from 'data-services/types'
44
import { getFetchUrl } from 'data-services/utils'
55
import { useMemo } from 'react'
6+
import { UserPermission } from 'utils/user/types'
67
import { useAuthorizedQuery } from '../auth/useAuthorizedQuery'
78

89
const convertServerRecord = (record: ServerCapture) => new Capture(record)
@@ -11,6 +12,7 @@ export const useCaptures = (
1112
params: FetchParams
1213
): {
1314
captures?: Capture[]
15+
userPermissions?: UserPermission[]
1416
total: number
1517
isLoading: boolean
1618
isFetching: boolean
@@ -20,6 +22,7 @@ export const useCaptures = (
2022

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

3235
return {
3336
captures,
37+
userPermissions: data?.user_permissions,
3438
total: data?.count ?? 0,
3539
isLoading,
3640
isFetching,

ui/src/data-services/hooks/captures/useUploadCapture.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const useUploadCapture = (onSuccess?: (id: string) => void) => {
1414
const { user } = useUser()
1515
const queryClient = useQueryClient()
1616

17-
const { mutate, isLoading, error, isSuccess } = useMutation({
17+
const { mutateAsync, isLoading, error, isSuccess, reset } = useMutation({
1818
mutationFn: (fieldValues: UploadCaptureFieldValues) => {
1919
const data = new FormData()
2020
if (fieldValues.projectId) {
@@ -41,10 +41,12 @@ export const useUploadCapture = (onSuccess?: (id: string) => void) => {
4141
)
4242
},
4343
onSuccess: ({ data }) => {
44+
queryClient.invalidateQueries([API_ROUTES.CAPTURES])
4445
queryClient.invalidateQueries([API_ROUTES.DEPLOYMENTS])
46+
queryClient.invalidateQueries([API_ROUTES.SUMMARY])
4547
onSuccess?.(`${data.source_image.id}`)
4648
},
4749
})
4850

49-
return { uploadCapture: mutate, isLoading, error, isSuccess }
51+
return { uploadCapture: mutateAsync, isLoading, error, isSuccess, reset }
5052
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { useQueryClient } from '@tanstack/react-query'
2+
import { API_ROUTES, SUCCESS_TIMEOUT } from 'data-services/constants'
3+
import { useState } from 'react'
4+
import { useUploadCapture } from './useUploadCapture'
5+
6+
type Result = PromiseSettledResult<any>
7+
8+
const isRejected = (result: Result) => result.status === 'rejected'
9+
10+
export const useUploadCaptures = (onSuccess?: () => void) => {
11+
const queryClient = useQueryClient()
12+
const [results, setResults] = useState<Result[]>()
13+
const { uploadCapture, isLoading, isSuccess, reset } = useUploadCapture(
14+
() => {
15+
setTimeout(() => {
16+
reset()
17+
}, SUCCESS_TIMEOUT)
18+
}
19+
)
20+
21+
const error = results?.some(isRejected)
22+
? 'Not all images could not be uploaded, please retry.'
23+
: undefined
24+
25+
return {
26+
isLoading,
27+
isSuccess,
28+
error,
29+
uploadCaptures: async (params: {
30+
projectId: string
31+
deploymentId: string
32+
files: File[]
33+
}) => {
34+
const promises = params.files
35+
.filter((_, index) => {
36+
if (error) {
37+
// Only retry rejected requests
38+
return results?.[index]?.status === 'rejected'
39+
}
40+
41+
return true
42+
})
43+
.map((file) =>
44+
uploadCapture({
45+
projectId: params.projectId,
46+
deploymentId: params.deploymentId,
47+
file,
48+
})
49+
)
50+
51+
setResults(undefined)
52+
const updatesResults = await Promise.allSettled(promises)
53+
setResults(updatesResults)
54+
55+
if (!updatesResults?.some(isRejected)) {
56+
queryClient.invalidateQueries([API_ROUTES.CAPTURES])
57+
queryClient.invalidateQueries([API_ROUTES.DEPLOYMENTS])
58+
queryClient.invalidateQueries([API_ROUTES.SUMMARY])
59+
queryClient.invalidateQueries([API_ROUTES.PROJECTS, params.projectId])
60+
onSuccess?.()
61+
}
62+
},
63+
}
64+
}

ui/src/data-services/hooks/projects/useCreateProject.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export const useCreateProject = (onSuccess?: () => void) => {
1111

1212
const { mutateAsync, isLoading, isSuccess, reset, error } = useMutation({
1313
mutationFn: (fieldValues: any) =>
14-
axios.post(
14+
axios.post<{ id: number }>(
1515
`${API_URL}/${API_ROUTES.PROJECTS}/`,
1616
convertToServerFormData(fieldValues),
1717
{

ui/src/data-services/models/collection.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,8 @@ export class Collection extends Entity {
8080
this.numImagesWithDetections && this.numImages
8181
? (this.numImagesWithDetections / this.numImages) * 100
8282
: 0
83-
return `${this.numImagesWithDetections?.toLocaleString()} / ${this.numImages?.toLocaleString()} (${pct.toFixed(
83+
84+
return `${this.numImagesWithDetections?.toLocaleString()} (${pct.toFixed(
8485
0
8586
)}%)`
8687
}

ui/src/data-services/models/project-details.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { UserPermission } from 'utils/user/types'
12
import { Project } from './project'
23

34
export type ServerProject = any // TODO: Update this type
@@ -51,4 +52,8 @@ export class ProjectDetails extends Project {
5152
get summaryData(): SummaryData[] {
5253
return this._project.summary_data
5354
}
55+
56+
get userPermissions(): UserPermission[] {
57+
return this._project.user_permissions
58+
}
5459
}

0 commit comments

Comments
 (0)