diff --git a/ui/src/components/form/layout/layout.module.scss b/ui/src/components/form/layout/layout.module.scss index 5a488a43a..60fdeaefd 100644 --- a/ui/src/components/form/layout/layout.module.scss +++ b/ui/src/components/form/layout/layout.module.scss @@ -57,9 +57,19 @@ gap: 16px; } +.formMessage { + width: 100%; + @include paragraph-small(); + padding: 8px 16px; + background-color: $color-success-100; + color: $color-success-700; + box-sizing: border-box; + border-radius: 6px; +} + .formError { width: 100%; - @include paragraph-x-small(); + @include paragraph-small(); padding: 8px 32px; background-color: $color-destructive-100; color: $color-destructive-600; @@ -72,6 +82,10 @@ } } +.intro { + font-weight: 600; +} + @media only screen and (max-width: $small-screen-breakpoint) { .section { margin: 16px; diff --git a/ui/src/components/form/layout/layout.tsx b/ui/src/components/form/layout/layout.tsx index 4cb7663dd..d58b7980d 100644 --- a/ui/src/components/form/layout/layout.tsx +++ b/ui/src/components/form/layout/layout.tsx @@ -2,6 +2,21 @@ import classNames from 'classnames' import { CSSProperties, ReactNode } from 'react' import styles from './layout.module.scss' +export const FormMessage = ({ + intro, + message, + style, +}: { + intro?: string + message: string + style?: CSSProperties +}) => ( +
+ {intro ? {intro}: : null} + {message} +
+) + export const FormError = ({ inDialog, intro, @@ -17,7 +32,8 @@ export const FormError = ({ className={classNames(styles.formError, { [styles.inDialog]: inDialog })} style={style} > - {intro ? `${intro}: ${message}` : message} + {intro ? {intro}: : null} + {message} ) diff --git a/ui/src/components/header/user-info-dialog/user-info-image-upload/user-info-image-upload.tsx b/ui/src/components/header/user-info-dialog/user-info-image-upload/user-info-image-upload.tsx index 97c2a742e..b11bcc930 100644 --- a/ui/src/components/header/user-info-dialog/user-info-image-upload/user-info-image-upload.tsx +++ b/ui/src/components/header/user-info-dialog/user-info-image-upload/user-info-image-upload.tsx @@ -28,7 +28,7 @@ export const UserInfoImageUpload = ({ return ( <> -
+
{imageUrl ? ( <> diff --git a/ui/src/data-services/hooks/captures/useUploadCaptures.ts b/ui/src/data-services/hooks/captures/useUploadCaptures.ts index 5062260f9..3429dfcec 100644 --- a/ui/src/data-services/hooks/captures/useUploadCaptures.ts +++ b/ui/src/data-services/hooks/captures/useUploadCaptures.ts @@ -10,7 +10,7 @@ const isRejected = (result: Result) => result.status === 'rejected' export const useUploadCaptures = (onSuccess?: () => void) => { const queryClient = useQueryClient() const [results, setResults] = useState() - const { uploadCapture, isLoading, isSuccess } = useUploadCapture() + const { uploadCapture, isLoading, isSuccess, reset } = useUploadCapture() const error = results?.some(isRejected) ? 'Not all images could be uploaded, please retry.' @@ -20,6 +20,10 @@ export const useUploadCaptures = (onSuccess?: () => void) => { isLoading, isSuccess, error, + reset: () => { + setResults([]) + reset() + }, uploadCaptures: async (params: { deploymentId: string files: File[] diff --git a/ui/src/data-services/models/capture-details.ts b/ui/src/data-services/models/capture-details.ts index c1fbc9fb0..cd7affa2c 100644 --- a/ui/src/data-services/models/capture-details.ts +++ b/ui/src/data-services/models/capture-details.ts @@ -28,7 +28,7 @@ export class CaptureDetails extends Capture { const time1 = j1.updatedAt?.getTime() ?? 0 const time2 = j2.updatedAt?.getTime() ?? 0 - return time1 - time2 + return time2 - time1 })[0] } diff --git a/ui/src/data-services/models/capture-set.ts b/ui/src/data-services/models/capture-set.ts index 743978731..1275f6003 100644 --- a/ui/src/data-services/models/capture-set.ts +++ b/ui/src/data-services/models/capture-set.ts @@ -32,7 +32,7 @@ export class CaptureSet extends Entity { const time1 = j1.updatedAt?.getTime() ?? 0 const time2 = j2.updatedAt?.getTime() ?? 0 - return time1 - time2 + return time2 - time1 })[0] } diff --git a/ui/src/data-services/models/deployment.ts b/ui/src/data-services/models/deployment.ts index 99b875f05..069896bf8 100644 --- a/ui/src/data-services/models/deployment.ts +++ b/ui/src/data-services/models/deployment.ts @@ -38,7 +38,7 @@ export class Deployment extends Entity { const time1 = j1.updatedAt?.getTime() ?? 0 const time2 = j2.updatedAt?.getTime() ?? 0 - return time1 - time2 + return time2 - time1 })[0] } diff --git a/ui/src/design-system/components/file-input/file-input.module.scss b/ui/src/design-system/components/file-input/file-input.module.scss index 9c01d070f..be0bcb29c 100644 --- a/ui/src/design-system/components/file-input/file-input.module.scss +++ b/ui/src/design-system/components/file-input/file-input.module.scss @@ -2,10 +2,11 @@ @import 'src/design-system/variables/typography.scss'; .container { + width: 100%; + height: 100%; display: flex; align-items: center; justify-content: flex-start; - margin-top: 8px; gap: 8px; } diff --git a/ui/src/design-system/components/image-upload/image-upload.tsx b/ui/src/design-system/components/image-upload/image-upload.tsx index 70a8ee183..26fec8c61 100644 --- a/ui/src/design-system/components/image-upload/image-upload.tsx +++ b/ui/src/design-system/components/image-upload/image-upload.tsx @@ -30,7 +30,7 @@ export const ImageUpload = ({ <>
diff --git a/ui/src/design-system/components/select/entity-picker.tsx b/ui/src/design-system/components/select/entity-picker.tsx index b151a7066..f0ba9d69e 100644 --- a/ui/src/design-system/components/select/entity-picker.tsx +++ b/ui/src/design-system/components/select/entity-picker.tsx @@ -5,7 +5,7 @@ import { STRING, translate } from 'utils/language' export const EntityPicker = ({ collection, - value, + value: _value, onValueChange, }: { collection: string @@ -16,12 +16,14 @@ export const EntityPicker = ({ const { entities = [], isLoading } = useEntities(collection, { projectId: projectId as string, }) + const value = entities.some((e) => e.id === _value) ? _value : '' return ( e.id === value) ? value : ''} + value={value} > diff --git a/ui/src/pages/captures/upload-images-dialog/select-images-section/select-images-section.tsx b/ui/src/pages/captures/upload-images-dialog/select-images-section/select-images-section.tsx index 7245360a9..24fce7e5d 100644 --- a/ui/src/pages/captures/upload-images-dialog/select-images-section/select-images-section.tsx +++ b/ui/src/pages/captures/upload-images-dialog/select-images-section/select-images-section.tsx @@ -2,7 +2,7 @@ import { FormSection } from 'components/form/layout/layout' import { FileInput } from 'design-system/components/file-input/file-input' import { FileInputAccept } from 'design-system/components/file-input/types' import { PlusIcon, XIcon } from 'lucide-react' -import { Button } from 'nova-ui-kit' +import { Button, buttonVariants } from 'nova-ui-kit' import { ReactNode } from 'react' import { API_MAX_UPLOAD_SIZE } from 'utils/constants' import { STRING, translate } from 'utils/language' @@ -69,14 +69,21 @@ export const SelectImagesSection = ({ multiple name="select-captures" renderInput={({ onClick }) => ( - +
+ +
+ )} onChange={(files) => { if (!files) { diff --git a/ui/src/pages/captures/upload-images-dialog/upload-images-dialog.tsx b/ui/src/pages/captures/upload-images-dialog/upload-images-dialog.tsx index 2222c3226..c9d3e1a3c 100644 --- a/ui/src/pages/captures/upload-images-dialog/upload-images-dialog.tsx +++ b/ui/src/pages/captures/upload-images-dialog/upload-images-dialog.tsx @@ -53,9 +53,17 @@ export const UploadImagesDialog = ({ !!project?.settings.defaultProcessingPipeline ) - const { uploadCaptures, isLoading, isSuccess, error } = useUploadCaptures() + const { + uploadCaptures, + isLoading, + isSuccess, + error, + reset: resetHook, + } = useUploadCaptures() + // Reset on open state change useEffect(() => { + resetHook() setCurrentSection(Section.Images) setImages([]) setDeployment(undefined) @@ -70,9 +78,7 @@ export const UploadImagesDialog = ({ - + {error ? : null}
{isSuccess ? ( diff --git a/ui/src/pages/deployments/deployment-columns.tsx b/ui/src/pages/deployments/deployment-columns.tsx index e8067d812..946eba0cd 100644 --- a/ui/src/pages/deployments/deployment-columns.tsx +++ b/ui/src/pages/deployments/deployment-columns.tsx @@ -77,7 +77,9 @@ export const columns = ({ { id: 'status', name: 'Latest job status', - tooltip: translate(STRING.TOOLTIP_STATUS), + tooltip: translate(STRING.TOOLTIP_LATEST_JOB_STATUS, { + type: translate(STRING.ENTITY_TYPE_DEPLOYMENT), + }), renderCell: (item: Deployment) => { if (!item.currentJob) { return <> diff --git a/ui/src/pages/jobs/jobs-columns.tsx b/ui/src/pages/jobs/jobs-columns.tsx index d19485a28..1240b71c5 100644 --- a/ui/src/pages/jobs/jobs-columns.tsx +++ b/ui/src/pages/jobs/jobs-columns.tsx @@ -36,7 +36,6 @@ export const columns = ({ { id: 'status', name: translate(STRING.FIELD_LABEL_STATUS), - tooltip: translate(STRING.TOOLTIP_STATUS), sortField: 'status', renderCell: (item: Job) => ( { if (!item.currentJob) { return <> diff --git a/ui/src/pages/project/entities/details-form/capture-set-details-form.tsx b/ui/src/pages/project/entities/details-form/capture-set-details-form.tsx index 03f2be3fe..1b6fd4a60 100644 --- a/ui/src/pages/project/entities/details-form/capture-set-details-form.tsx +++ b/ui/src/pages/project/entities/details-form/capture-set-details-form.tsx @@ -3,6 +3,7 @@ import { FormField } from 'components/form/form-field' import { FormActions, FormError, + FormMessage, FormRow, FormSection, } from 'components/form/layout/layout' @@ -204,6 +205,9 @@ export const CaptureSetDetailsForm = ({ /> )} +

General

@@ -365,6 +369,10 @@ export const CaptureSetDetailsForm = ({ /> ) : null} +
diff --git a/ui/src/pages/project/entities/details-form/export-details-form.tsx b/ui/src/pages/project/entities/details-form/export-details-form.tsx index 55a625471..bf7c4638f 100644 --- a/ui/src/pages/project/entities/details-form/export-details-form.tsx +++ b/ui/src/pages/project/entities/details-form/export-details-form.tsx @@ -2,6 +2,7 @@ import { FormController } from 'components/form/form-controller' import { FormActions, FormError, + FormMessage, FormSection, } from 'components/form/layout/layout' import { FormConfig } from 'components/form/types' @@ -121,6 +122,10 @@ export const ExportDetailsForm = ({ )} /> + diff --git a/ui/src/pages/session-details/playback/capture-details/capture-details.module.scss b/ui/src/pages/session-details/playback/capture-details/capture-details.module.scss index 934c8e976..f8a65c42e 100644 --- a/ui/src/pages/session-details/playback/capture-details/capture-details.module.scss +++ b/ui/src/pages/session-details/playback/capture-details/capture-details.module.scss @@ -14,20 +14,16 @@ display: flex; flex-direction: column; gap: 16px; + overflow: hidden; } .jobControls { - display: grid; - grid-template-columns: 1fr auto; + display: flex; + flex-wrap: wrap; gap: 16px; margin-top: 4px; } -.pipelinesPickerContainer { - max-width: 100%; - overflow: auto; -} - .label { display: block; @include paragraph-xx-small(); @@ -53,15 +49,6 @@ } @media only screen and (max-width: $small-screen-breakpoint) { - .jobControls { - grid-template-columns: 1fr auto; - gap: 16px; - } - - .pipelinesPickerContainer { - grid-column: span 2; - } - .infoWrapper { display: none; } diff --git a/ui/src/pages/session-details/playback/capture-details/capture-details.tsx b/ui/src/pages/session-details/playback/capture-details/capture-details.tsx index 8cce01bcc..2dc0e3742 100644 --- a/ui/src/pages/session-details/playback/capture-details/capture-details.tsx +++ b/ui/src/pages/session-details/playback/capture-details/capture-details.tsx @@ -243,10 +243,10 @@ const PipelinesPicker = ({ value={value ?? ''} > - + {pipelines.map((p) => ( diff --git a/ui/src/pages/session-details/playback/playback.module.scss b/ui/src/pages/session-details/playback/playback.module.scss index 3296dc409..40410dc9f 100644 --- a/ui/src/pages/session-details/playback/playback.module.scss +++ b/ui/src/pages/session-details/playback/playback.module.scss @@ -27,6 +27,7 @@ gap: 64px; box-sizing: border-box; overflow-y: auto; + overflow-x: hidden; } .sidebarSection { diff --git a/ui/src/pages/session-details/playback/session-captures-slider/session-captures-slider.tsx b/ui/src/pages/session-details/playback/session-captures-slider/session-captures-slider.tsx index 59c387bcb..f7976bd49 100644 --- a/ui/src/pages/session-details/playback/session-captures-slider/session-captures-slider.tsx +++ b/ui/src/pages/session-details/playback/session-captures-slider/session-captures-slider.tsx @@ -22,6 +22,7 @@ export const SessionCapturesSlider = ({ const [value, setValue] = useState(0) const startDate = session.startDate const endDate = session.endDate + const showLabels = session.startDate.getTime() !== session.endDate.getTime() useEffect(() => { if (activeCapture) { @@ -32,10 +33,14 @@ export const SessionCapturesSlider = ({ return (
- ((date.getTime() - startDate.getTime()) / - (endDate.getTime() - startDate.getTime())) * - 100 +}) => { + if (endDate.getTime() === startDate.getTime()) { + return 50 + } + + return ( + ((date.getTime() - startDate.getTime()) / + (endDate.getTime() - startDate.getTime())) * + 100 + ) +} export const valueToDate = ({ value, diff --git a/ui/src/pages/session-details/session-details.tsx b/ui/src/pages/session-details/session-details.tsx index d0e693241..f6864578c 100644 --- a/ui/src/pages/session-details/session-details.tsx +++ b/ui/src/pages/session-details/session-details.tsx @@ -1,5 +1,4 @@ import { ErrorState } from 'components/error-state/error-state' -import { FetchInfo } from 'components/fetch-info/fetch-info' import { useSessionDetails } from 'data-services/hooks/sessions/useSessionDetails' import { Box } from 'design-system/components/box/box' import { LoadingSpinner } from 'design-system/components/loading-spinner/loading-spinner' @@ -20,10 +19,10 @@ export const SessionDetails = () => { const { setDetailBreadcrumb } = useContext(BreadcrumbContext) const { activeOccurrences } = useActiveOccurrences() const { activeCaptureId } = useActiveCaptureId() - const { session, isLoading, isFetching, error } = useSessionDetails( - id as string, - { capture: activeCaptureId, occurrence: activeOccurrences[0] } - ) + const { session, isLoading, error } = useSessionDetails(id as string, { + capture: activeCaptureId, + occurrence: activeOccurrences[0], + }) useEffect(() => { setDetailBreadcrumb(session ? { title: session.label } : undefined) @@ -50,11 +49,6 @@ export const SessionDetails = () => { - {isFetching && ( -
- -
- )}
diff --git a/ui/src/utils/language.ts b/ui/src/utils/language.ts index 9f955f0fa..f9a90afa0 100644 --- a/ui/src/utils/language.ts +++ b/ui/src/utils/language.ts @@ -167,6 +167,8 @@ export enum STRING { /* MESSAGE */ MESSAGE_CAPTURE_FILENAME, MESSAGE_CAPTURE_LIMIT, + MESSAGE_CAPTURE_SET_FORM_INTRO, + MESSAGE_CAPTURE_SET_TIP, MESSAGE_CAPTURE_SYNC_HIDDEN, MESSAGE_CAPTURE_TOO_MANY, MESSAGE_CAPTURE_UPLOAD_HIDDEN, @@ -177,6 +179,7 @@ export enum STRING { MESSAGE_DEFAULT_PIPELINE, MESSAGE_DELETE_CONFIRM, MESSAGE_DRAFTS, + MESSAGE_EXPORT_TIP, MESSAGE_HAS_ACCOUNT, MESSAGE_IMAGE_FORMAT, MESSAGE_IMAGE_SIZE, @@ -254,13 +257,14 @@ export enum STRING { TOOLTIP_DEPLOYMENT, TOOLTIP_DEVICE, TOOLTIP_JOB, + TOOLTIP_LATEST_JOB_STATUS, TOOLTIP_OCCURRENCE, TOOLTIP_PIPELINE, TOOLTIP_PROCESSING_SERVICE, TOOLTIP_SCORE, TOOLTIP_SESSION, TOOLTIP_SITE, - TOOLTIP_STATUS, + TOOLTIP_STORAGE, /* OTHER */ @@ -312,6 +316,7 @@ export enum STRING { SUMMARY, TABLE_COLUMNS, TERMINAL_CLASSIFICATION, + TIP, UNKNOWN_ERROR, UNKNOWN, UPDATING_DATA, @@ -495,6 +500,10 @@ const ENGLISH_STRINGS: { [key in STRING]: string } = { 'Image filename must contain a timestamp with year, month, day, hours, minutes and seconds (e.g. 20210101120000-snapshot.jpg).', [STRING.MESSAGE_CAPTURE_LIMIT]: 'A maximum of {{numCaptures}} captures for each station can be uploaded through the web browser. Configure a data source to upload data in bulk.', + [STRING.MESSAGE_CAPTURE_SET_FORM_INTRO]: + 'In this form, you will define the logic for your capture set. When the capture set is defined, it can be populated with captures from the table view.', + [STRING.MESSAGE_CAPTURE_SET_TIP]: + 'To define a capture set for all captures, use method "Full" without setting filters.', [STRING.MESSAGE_CAPTURE_SYNC_HIDDEN]: 'Station must be created before syncing captures.', [STRING.MESSAGE_CAPTURE_TOO_MANY]: @@ -513,6 +522,7 @@ const ENGLISH_STRINGS: { [key in STRING]: string } = { [STRING.MESSAGE_DELETE_CONFIRM]: 'Are you sure you want to delete this {{type}}?', [STRING.MESSAGE_DRAFTS]: 'Drafts are private and limited to one user.', + [STRING.MESSAGE_EXPORT_TIP]: 'To include all captures, skip "Capture set".', [STRING.MESSAGE_HAS_ACCOUNT]: 'Already have an account?', [STRING.MESSAGE_IMAGE_FORMAT]: 'Valid formats are PNG, GIF and JPEG.', [STRING.MESSAGE_IMAGE_SIZE]: @@ -606,7 +616,9 @@ const ENGLISH_STRINGS: { [key in STRING]: string } = { [STRING.TOOLTIP_DEVICE]: 'A device type is the type of equipment or camera used for collecting captures. One or many deployments can be connected to a device type. Device type refers to the model version, category or description of a kind of hardware, not the serial number of an individual device.', [STRING.TOOLTIP_JOB]: - 'A job is a request for data processing that specifies the data to process and the pipeline to use.', + 'A job is a task that requires time to complete and runs in the background. Examples include processing captures, syncing captures, and generating exports.', + [STRING.TOOLTIP_LATEST_JOB_STATUS]: + 'A job is a task that requires time to complete and runs in the background. This shows the status of the latest job for each {{type}}. Hover the status label for more details about the job type.', [STRING.TOOLTIP_OCCURRENCE]: 'An occurrence refers to when an individual is detected in a sequence of one or more captures with no time interruption.', [STRING.TOOLTIP_PIPELINE]: @@ -619,8 +631,6 @@ const ENGLISH_STRINGS: { [key in STRING]: string } = { 'A session is a fixed period of time of monitoring for one station. The period is typically one night.', [STRING.TOOLTIP_SITE]: 'A site is a physical location where monitoring is taking place. One or many stations can be connected to a site.', - [STRING.TOOLTIP_STATUS]: - 'A status is the processing stage of a job once submitted: Created > Pending > Started > Success. A Failed status means the job stopped before it had finished.', [STRING.TOOLTIP_STORAGE]: 'A storage is a place where captures are kept, for example a S3 bucket. One or many stations can be connected to a storage.', @@ -673,6 +683,7 @@ const ENGLISH_STRINGS: { [key in STRING]: string } = { [STRING.SUMMARY]: 'Summary', [STRING.TABLE_COLUMNS]: 'Table columns', [STRING.TERMINAL_CLASSIFICATION]: 'Terminal classification', + [STRING.TIP]: 'Tip', [STRING.UNKNOWN_ERROR]: 'Unknown error', [STRING.UNKNOWN]: 'Unknown', [STRING.UPDATING_DATA]: 'Updating data', diff --git a/ui/src/utils/useFilters.ts b/ui/src/utils/useFilters.ts index 393ac9490..656e5c6f0 100644 --- a/ui/src/utils/useFilters.ts +++ b/ui/src/utils/useFilters.ts @@ -185,7 +185,6 @@ export const AVAILABLE_FILTERS = (projectId: string): FilterConfig[] => [ { label: 'Status', field: 'status', - tooltip: { text: translate(STRING.TOOLTIP_STATUS) }, }, { label: 'Type',