diff --git a/ui/src/data-services/constants.ts b/ui/src/data-services/constants.ts index f01558bdf..44b77c573 100644 --- a/ui/src/data-services/constants.ts +++ b/ui/src/data-services/constants.ts @@ -4,6 +4,7 @@ export const API_ROUTES = { CAPTURES: 'captures', COLLECTIONS: 'captures/collections', DEPLOYMENTS: 'deployments', + DEVICES: 'deployments/devices', IDENTIFICATIONS: 'identifications', JOBS: 'jobs', LOGIN: 'auth/token/login', @@ -14,7 +15,9 @@ export const API_ROUTES = { PIPELINES: 'ml/pipelines', PROJECTS: 'projects', SESSIONS: 'events', + SITES: 'deployments/sites', SPECIES: 'taxa', + STORAGE: 'storage', SUMMARY: 'status/summary', USERS: 'users', } diff --git a/ui/src/data-services/hooks/deployments/useCreateDeployment.ts b/ui/src/data-services/hooks/deployments/useCreateDeployment.ts index 946fcbc75..30bfb6ab9 100644 --- a/ui/src/data-services/hooks/deployments/useCreateDeployment.ts +++ b/ui/src/data-services/hooks/deployments/useCreateDeployment.ts @@ -6,14 +6,15 @@ import { getAuthHeader } from 'data-services/utils' import { useUser } from 'utils/user/userContext' const convertToServerFieldValues = (fieldValues: DeploymentFieldValues) => ({ - data_source: fieldValues.path, - description: fieldValues.description, + data_source_id: fieldValues.dataSourceId, + device_id: fieldValues.deviceId, events: [], name: fieldValues.name, latitude: fieldValues.latitude, longitude: fieldValues.longitude, occurrences: [], project_id: fieldValues.projectId, + research_site_id: fieldValues.siteId, }) export const useCreateDeployment = () => { diff --git a/ui/src/data-services/hooks/deployments/useUpdateDeployment.ts b/ui/src/data-services/hooks/deployments/useUpdateDeployment.ts index 2c02713a1..9db8d777b 100644 --- a/ui/src/data-services/hooks/deployments/useUpdateDeployment.ts +++ b/ui/src/data-services/hooks/deployments/useUpdateDeployment.ts @@ -6,11 +6,13 @@ import { getAuthHeader } from 'data-services/utils' import { useUser } from 'utils/user/userContext' const convertToServerFieldValues = (fieldValues: DeploymentFieldValues) => ({ - data_source: fieldValues.path, + data_source_id: fieldValues.dataSourceId, description: fieldValues.description, + device_id: fieldValues.deviceId, name: fieldValues.name, latitude: fieldValues.latitude, longitude: fieldValues.longitude, + research_site_id: fieldValues.siteId, }) export const useUpdateDeployment = (id: string) => { diff --git a/ui/src/data-services/hooks/entities/types.ts b/ui/src/data-services/hooks/entities/types.ts new file mode 100644 index 000000000..9230a6173 --- /dev/null +++ b/ui/src/data-services/hooks/entities/types.ts @@ -0,0 +1,6 @@ +export interface EntityFieldValues { + description: string + name: string + projectId: string + customFields?: { [key: string]: string } +} diff --git a/ui/src/data-services/hooks/entities/useCreateEntity.ts b/ui/src/data-services/hooks/entities/useCreateEntity.ts new file mode 100644 index 000000000..5943248ba --- /dev/null +++ b/ui/src/data-services/hooks/entities/useCreateEntity.ts @@ -0,0 +1,32 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import axios from 'axios' +import { API_URL } from 'data-services/constants' +import { getAuthHeader } from 'data-services/utils' +import { useUser } from 'utils/user/userContext' +import { EntityFieldValues } from './types' +import { convertToServerFieldValues } from './utils' + +const SUCCESS_TIMEOUT = 1000 // Reset success after 1 second + +export const useCreateEntity = (collection: string, onSuccess?: () => void) => { + const { user } = useUser() + const queryClient = useQueryClient() + + const { mutateAsync, isLoading, isSuccess, reset, error } = useMutation({ + mutationFn: (fieldValues: EntityFieldValues) => + axios.post( + `${API_URL}/${collection}/`, + convertToServerFieldValues(fieldValues), + { + headers: getAuthHeader(user), + } + ), + onSuccess: () => { + queryClient.invalidateQueries([collection]) + onSuccess?.() + setTimeout(reset, SUCCESS_TIMEOUT) + }, + }) + + return { createEntity: mutateAsync, isLoading, isSuccess, error } +} diff --git a/ui/src/data-services/hooks/entities/useDeleteEntity.ts b/ui/src/data-services/hooks/entities/useDeleteEntity.ts new file mode 100644 index 000000000..4c1ee6747 --- /dev/null +++ b/ui/src/data-services/hooks/entities/useDeleteEntity.ts @@ -0,0 +1,23 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import axios from 'axios' +import { API_URL } from 'data-services/constants' +import { getAuthHeader } from 'data-services/utils' +import { useUser } from 'utils/user/userContext' + +export const useDeleteEntity = (collection: string, onSuccess?: () => void) => { + const { user } = useUser() + const queryClient = useQueryClient() + + const { mutateAsync, isLoading, isSuccess, error } = useMutation({ + mutationFn: (id: string) => + axios.delete(`${API_URL}/${collection}/${id}/`, { + headers: getAuthHeader(user), + }), + onSuccess: () => { + queryClient.invalidateQueries([collection]) + onSuccess?.() + }, + }) + + return { deleteEntity: mutateAsync, isLoading, isSuccess, error } +} diff --git a/ui/src/data-services/hooks/entities/useEntities.ts b/ui/src/data-services/hooks/entities/useEntities.ts new file mode 100644 index 000000000..17931e3de --- /dev/null +++ b/ui/src/data-services/hooks/entities/useEntities.ts @@ -0,0 +1,49 @@ +import { Entity, ServerEntity } from 'data-services/models/entity' +import { Storage } from 'data-services/models/storage' +import { FetchParams } from 'data-services/types' +import { getFetchUrl } from 'data-services/utils' +import { useMemo } from 'react' +import { useAuthorizedQuery } from '../auth/useAuthorizedQuery' + +const convertServerRecord = (collection: string, record: ServerEntity) => { + if (collection === 'storage') { + return new Storage(record) + } + + return new Entity(record) +} + +export const useEntities = ( + collection: string, + params?: FetchParams +): { + entities?: Entity[] + total: number + isLoading: boolean + isFetching: boolean + error?: unknown +} => { + const fetchUrl = getFetchUrl({ collection, params }) + + const { data, isLoading, isFetching, error } = useAuthorizedQuery<{ + results: ServerEntity[] + count: number + }>({ + queryKey: [collection, params], + url: fetchUrl, + }) + + const entities = useMemo( + () => + data?.results.map((record) => convertServerRecord(collection, record)), + [data] + ) + + return { + entities, + total: data?.count ?? 0, + isLoading, + isFetching, + error, + } +} diff --git a/ui/src/data-services/hooks/entities/useUpdateEntity.ts b/ui/src/data-services/hooks/entities/useUpdateEntity.ts new file mode 100644 index 000000000..087e39dd9 --- /dev/null +++ b/ui/src/data-services/hooks/entities/useUpdateEntity.ts @@ -0,0 +1,36 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query' +import axios from 'axios' +import { API_URL } from 'data-services/constants' +import { getAuthHeader } from 'data-services/utils' +import { useUser } from 'utils/user/userContext' +import { EntityFieldValues } from './types' +import { convertToServerFieldValues } from './utils' + +const SUCCESS_TIMEOUT = 1000 // Reset success after 1 second + +export const useUpdateEntity = ( + id: string, + collection: string, + onSuccess?: () => void +) => { + const { user } = useUser() + const queryClient = useQueryClient() + + const { mutateAsync, isLoading, isSuccess, reset, error } = useMutation({ + mutationFn: (fieldValues: EntityFieldValues) => + axios.patch( + `${API_URL}/${collection}/${id}/`, + convertToServerFieldValues(fieldValues), + { + headers: getAuthHeader(user), + } + ), + onSuccess: () => { + queryClient.invalidateQueries([collection]) + onSuccess?.() + setTimeout(reset, SUCCESS_TIMEOUT) + }, + }) + + return { updateEntity: mutateAsync, isLoading, error, isSuccess } +} diff --git a/ui/src/data-services/hooks/entities/utils.ts b/ui/src/data-services/hooks/entities/utils.ts new file mode 100644 index 000000000..eef89e8c0 --- /dev/null +++ b/ui/src/data-services/hooks/entities/utils.ts @@ -0,0 +1,10 @@ +import { EntityFieldValues } from './types' + +export const convertToServerFieldValues = (fieldValues: EntityFieldValues) => { + return { + description: fieldValues.description, + name: fieldValues.name, + project: fieldValues.projectId, + ...(fieldValues.customFields ?? {}), + } +} diff --git a/ui/src/data-services/models/deployment-details.ts b/ui/src/data-services/models/deployment-details.ts index 546a5696c..c391dd94a 100644 --- a/ui/src/data-services/models/deployment-details.ts +++ b/ui/src/data-services/models/deployment-details.ts @@ -1,13 +1,17 @@ import { Deployment, ServerDeployment } from './deployment' +import { Entity } from './entity' +import { Storage } from './storage' export type ServerDeploymentDetails = ServerDeployment & any // TODO: Update this type export interface DeploymentFieldValues { + dataSourceId?: string description: string name: string + siteId?: string + deviceId?: string latitude: number longitude: number - path: string projectId?: string } @@ -27,6 +31,12 @@ export class DeploymentDetails extends Deployment { } } + get device(): Entity | undefined { + if (this._deployment.device) { + return new Entity(this._deployment.device) + } + } + get description(): string { return this._deployment.description } @@ -35,7 +45,15 @@ export class DeploymentDetails extends Deployment { return this._exampleCaptures } - get path(): string { - return this._deployment.data_source + get dataSource(): Storage | undefined { + if (this._deployment.data_source) { + return new Storage(this._deployment.data_source) + } + } + + get site(): Entity | undefined { + if (this._deployment.research_site) { + return new Entity(this._deployment.research_site) + } } } diff --git a/ui/src/data-services/models/entity.ts b/ui/src/data-services/models/entity.ts new file mode 100644 index 000000000..62c0400ac --- /dev/null +++ b/ui/src/data-services/models/entity.ts @@ -0,0 +1,44 @@ +import { getFormatedDateTimeString } from 'utils/date/getFormatedDateTimeString/getFormatedDateTimeString' +import { UserPermission } from 'utils/user/types' + +export type ServerEntity = any // TODO: Update this type + +export class Entity { + protected readonly _data: ServerEntity + + public constructor(entity: ServerEntity) { + this._data = entity + } + + get canDelete(): boolean { + return this._data.user_permissions.includes(UserPermission.Delete) + } + + get createdAt(): string { + return getFormatedDateTimeString({ + date: new Date(this._data.created_at), + }) + } + + get description(): string { + return this._data.description + } + + get id(): string { + return `${this._data.id}` + } + + get name(): string { + return this._data.name + } + + get updatedAt(): string | undefined { + if (!this._data.updated_at) { + return undefined + } + + return getFormatedDateTimeString({ + date: new Date(this._data.updated_at), + }) + } +} diff --git a/ui/src/data-services/models/storage.ts b/ui/src/data-services/models/storage.ts new file mode 100644 index 000000000..bb7f2121b --- /dev/null +++ b/ui/src/data-services/models/storage.ts @@ -0,0 +1,21 @@ +import { Entity } from './entity' + +export type ServerStorage = any // TODO: Update this type + +export class Storage extends Entity { + public constructor(entity: Storage) { + super(entity) + } + + get bucket(): string { + return this._data.bucket + } + + get endpointUrl(): string { + return this._data.endpoint_url + } + + get publicBaseUrl(): string { + return this._data.public_base_url + } +} diff --git a/ui/src/pages/deployment-details/deployment-details-form/config.ts b/ui/src/pages/deployment-details/deployment-details-form/config.ts index facf20806..b9c4e735e 100644 --- a/ui/src/pages/deployment-details/deployment-details-form/config.ts +++ b/ui/src/pages/deployment-details/deployment-details-form/config.ts @@ -9,6 +9,15 @@ export const config: FormConfig = { description: { label: translate(STRING.FIELD_LABEL_DESCRIPTION), }, + siteId: { + label: translate(STRING.FIELD_LABEL_SITE), + }, + deviceId: { + label: translate(STRING.FIELD_LABEL_DEVICE), + }, + dataSourceId: { + label: 'Data source', + }, latitude: { label: translate(STRING.FIELD_LABEL_LATITUDE), rules: { min: -90, max: 90 }, diff --git a/ui/src/pages/deployment-details/deployment-details-form/deployment-details-form.tsx b/ui/src/pages/deployment-details/deployment-details-form/deployment-details-form.tsx index 3264fdaa0..0785d369e 100644 --- a/ui/src/pages/deployment-details/deployment-details-form/deployment-details-form.tsx +++ b/ui/src/pages/deployment-details/deployment-details-form/deployment-details-form.tsx @@ -40,6 +40,8 @@ export const DeploymentDetailsForm = ({ values: { name: deployment.name, description: deployment.description, + deviceId: deployment.device?.id, + siteId: deployment.site?.id, }, isValid: startValid, }, @@ -52,7 +54,7 @@ export const DeploymentDetailsForm = ({ }, [Section.SourceImages]: { values: { - path: deployment.path, + dataSourceId: deployment.dataSource?.id, }, isValid: startValid, }, diff --git a/ui/src/pages/deployment-details/deployment-details-form/section-general/section-general.tsx b/ui/src/pages/deployment-details/deployment-details-form/section-general/section-general.tsx index 8df8091f3..dde863ff3 100644 --- a/ui/src/pages/deployment-details/deployment-details-form/section-general/section-general.tsx +++ b/ui/src/pages/deployment-details/deployment-details-form/section-general/section-general.tsx @@ -1,12 +1,16 @@ +import { FormController } from 'components/form/form-controller' import { FormField } from 'components/form/form-field' import { FormActions, FormRow, FormSection, } from 'components/form/layout/layout' +import { API_ROUTES } from 'data-services/constants' import { DeploymentFieldValues } from 'data-services/models/deployment-details' import { Button, ButtonTheme } from 'design-system/components/button/button' +import { InputContent } from 'design-system/components/input/input' import _ from 'lodash' +import { EntitiesPicker } from 'pages/overview/entities/entities-picker' import { useContext } from 'react' import { useForm } from 'react-hook-form' import { FormContext } from 'utils/formContext/formContext' @@ -18,7 +22,7 @@ import { Section } from '../types' type SectionGeneralFieldValues = Pick< DeploymentFieldValues, - 'name' | 'description' + 'name' | 'description' | 'siteId' | 'deviceId' > const DEFAULT_VALUES: SectionGeneralFieldValues = { @@ -52,6 +56,44 @@ export const SectionGeneral = ({ onNext }: { onNext: () => void }) => { + + ( + + + + )} + /> + ( + + + + )} + /> +