Skip to content

Commit

Permalink
Implement STAC spec Assets (#28)
Browse files Browse the repository at this point in the history
* Asset form

* Implement assets with STAC spec

* Update package name
  • Loading branch information
RobertBroersma authored Nov 14, 2023
1 parent 4f96391 commit 08ea405
Show file tree
Hide file tree
Showing 21 changed files with 261 additions and 44 deletions.
112 changes: 105 additions & 7 deletions app/forms/items/ItemForm.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
import { Link } from '@remix-run/react'
import { Button } from '~/components/ui/button'
import type { z } from 'zod'
import type { ActionFunctionArgs, SerializeFrom } from '@remix-run/node'
import { db } from '~/utils/db.server'
import { updateGeometry } from '~/services/item.server'
import { ValidatedForm, validationError } from 'remix-validated-form'
import { withZod } from '@remix-validated-form/with-zod'
import { FormSubmit } from '~/components/ui/form'
import { FormInput, FormSubmit, FormTextarea } from '~/components/ui/form'
import { CollectionSelector } from '~/components/CollectionSelector'
import { Separator } from '~/components/ui/separator'
import { Role, type Collection, type Prisma } from '@prisma/client'
import { BoundsSelector } from '~/components/BoundsSelector/BoundsSelector'
import { DateRangePicker } from '~/components/DateRangePicker'
import { requestJsonOrFormData } from '~/utils/requestJsonOrFormdata'
import { requireAuthentication } from '~/services/auth.server'
import type { StacItem } from '~/utils/prismaToStac'
import { prismaToStacItem } from '~/utils/prismaToStac'
import { formTypes, createItemFormSchema } from '.'
import { Label } from '~/components/ui/label'
import React from 'react'
import { Plus, X } from 'lucide-react'
import { randUuid } from '@ngneat/falso'

export async function submitItemForm({
request,
Expand Down Expand Up @@ -79,12 +80,37 @@ export async function submitItemForm({
collectionId: collection,
}

let assets = Object.entries(form.data.assets ?? {}).map(
([objectKey, { key, ...asset }]) => ({
...asset,
key: key ?? objectKey,
roles:
typeof asset.roles === 'string'
? asset.roles?.split(',') ?? []
: asset.roles ?? [],
}),
)

let item = await db.item.upsert({
where: {
id: id ?? '',
},
create: data,
update: data,
create: {
...data,
assets: {
create: assets,
},
},
update: {
...data,
assets: {
deleteMany: {},
create: assets,
},
},
include: {
assets: true,
},
})

await updateGeometry({
Expand All @@ -105,7 +131,7 @@ export function ItemForm({
collections: SerializeFrom<
Collection & { catalog: { title: string | null } }
>[]
defaultValues?: unknown
defaultValues?: StacItem & {}
}) {
let [extraFormTypes, setExtraFormTypes] = React.useState<
(keyof typeof formTypes)[]
Expand All @@ -119,10 +145,14 @@ export function ItemForm({
) {
return key
}

return null
})
.filter(Boolean) as (keyof typeof formTypes)[],
)

let [assets, setAssets] = React.useState(defaultValues?.assets ?? {})

let itemSchema = React.useMemo(
() => createItemFormSchema(extraFormTypes),
[extraFormTypes],
Expand All @@ -149,14 +179,17 @@ export function ItemForm({
id="myform"
method="post"
validator={itemValidator}
defaultValues={defaultValues as z.infer<typeof itemSchema>}
defaultValues={{
...defaultValues,
assets,
}}
className="flex flex-col gap-y-16"
>
{extraFormTypes.map(formType => (
<input
key={formType}
type="hidden"
name="properties[__extraFormTypes]"
name="properties.__extraFormTypes"
value={formType}
/>
))}
Expand Down Expand Up @@ -194,6 +227,71 @@ export function ItemForm({
</div>
</div>

<div id="assets">
<h3 className="text-lg font-medium">Assets</h3>
<p className="text-sm text-muted-foreground">
Data associated with the item
</p>
</div>
<div className="col-span-2 flex flex-col gap-6">
{Object.entries(assets).map(([key], index) => (
<div
key={key}
data-testid={`asset-form-${index}`}
className="flex flex-col gap-6"
>
{index > 0 && <Separator className="my-12" />}
<div className="flex items-end gap-6">
<div className="flex-1">
<FormInput label="Key" name={`assets.${key}.key`} />
</div>
<Button
type="button"
variant="outline"
onClick={() =>
setAssets(c => {
let { [key]: _, ...rest } = c

return rest
})
}
>
<X className="w-4 h-4 mr-1.5" /> Remove
</Button>
</div>
<FormInput label="Link" name={`assets.${key}.href`} />
<FormInput label="Title" name={`assets.${key}.title`} />
<FormTextarea
label="Description"
name={`assets.${key}.description`}
/>
<div className="grid grid-cols-2 gap-6">
<FormInput
label="Type"
name={`assets.${key}.type`}
helper="E.g. application/geo+json"
/>
<FormInput
label="Roles"
name={`assets.${key}.roles`}
helper="A comma-separated list of semantic roles"
/>
</div>
</div>
))}

<div>
<Button
type="button"
onClick={() => {
setAssets(c => ({ ...c, [randUuid()]: {} }))
}}
>
<Plus className="w-4 h-4 mr-1.5" /> Add Asset
</Button>
</div>
</div>

{extraFormTypes?.map(type => {
let formType = formTypes[type]

Expand Down
13 changes: 4 additions & 9 deletions app/forms/items/NumericalModelItemForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ export let propertiesSchema = z.object({
description: z.string().optional(),
license: z.string().optional(),
projectNumber: z.string().optional(),
location: z.string().optional(),
contact: z.string().optional(),
reportLocation: z.string().optional(),
timeScale: z
Expand All @@ -25,18 +24,14 @@ export let propertiesSchema = z.object({

export function Form() {
return (
<div className="grid w-full items-center gap-6">
<div
className="grid w-full items-center gap-6"
data-testid="numerical-model-form"
>
<FormInput name="properties[title]" label="Title" />
<FormTextarea name="properties[description]" label="Description" />
<FormTextarea name="properties[license]" label="License" />
<FormInput name="properties[projectNumber]" label="Project Number" />
<FormInput
name="properties[location]"
label="Location"
placeholder="P://12345678-experiment"
helper="E.g. a path location on the P-drive (starting with P://) or a
bucket URL from MinIO."
/>
<FormInput name="properties[contact]" label="Contact" />
<FormInput name="properties[reportLocation]" label="Report Location" />
<FormInput name="properties[model]" label="Model" />
Expand Down
13 changes: 13 additions & 0 deletions app/forms/items/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,19 @@ export function createItemFormSchema(extraFormTypes: string[] = []) {
return z.object({
collection: z.string().min(1, { message: 'Please select a collection' }),
geometry: geometrySchema,
assets: z
.record(
z.string(),
z.object({
key: z.string().nullish(),
href: z.string().min(1, { message: 'Required' }),
title: z.string().nullish(),
description: z.string().nullish(),
type: z.string().nullish(),
roles: z.union([z.string(), z.array(z.string()).nullish()]),
}),
)
.optional(),
properties:
properties ??
z
Expand Down
9 changes: 6 additions & 3 deletions app/routes/api.search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,16 @@ export async function loader({ request }: LoaderFunctionArgs) {
LEFT JOIN "Permission" ON "Permission"."catalogId" = "Catalog"."id"
LEFT JOIN "Group" ON "Group"."id" = "Permission"."groupId"
LEFT JOIN "Member" ON "Member"."groupId" = "Group"."id"
WHERE ST_Intersects("Item"."geometry", ST_MakeEnvelope(${bbox[0].toFixed(
12,
)}::double precision, ${bbox[1].toFixed(
WHERE (
ST_Intersects("Item"."geometry", ST_MakeEnvelope(${bbox[0].toFixed(
12,
)}::double precision, ${bbox[1].toFixed(
12,
)}::double precision, ${bbox[2].toFixed(
12,
)}::double precision, ${bbox[3].toFixed(12)}::double precision, 4326))
OR "Item"."geometry" IS NULL
)
AND (
(
Expand Down
4 changes: 2 additions & 2 deletions app/routes/app.collections.$collectionId.edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { submitCatalogForm } from '~/forms/CatalogForm'
import { CollectionForm } from '~/forms/CollectionForm'
import { routes } from '~/routes'
import { requireAuthentication } from '~/services/auth.server'
import { getCollectionAuthWhere } from '~/utils/authQueries'
import { getCollectionAuthReadWhere } from '~/utils/authQueries'
import { db } from '~/utils/db.server'

export async function action(args: ActionFunctionArgs) {
Expand Down Expand Up @@ -35,7 +35,7 @@ export async function loader({ params, request }: LoaderFunctionArgs) {
},
}),
db.catalog.findMany({
where: getCollectionAuthWhere(user.id).catalog,
where: getCollectionAuthReadWhere(user.id).catalog,
}),
])

Expand Down
4 changes: 2 additions & 2 deletions app/routes/app.collections._index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,15 @@ import {
} from '~/components/ui/dropdown-menu'
import { routes } from '~/routes'
import { requireAuthentication } from '~/services/auth.server'
import { getCollectionAuthWhere } from '~/utils/authQueries'
import { getCollectionAuthReadWhere } from '~/utils/authQueries'
import { getDataTableFilters } from '~/utils/dataTableFilters'
import { db } from '~/utils/db.server'

export async function loader({ request }: LoaderFunctionArgs) {
let user = await requireAuthentication(request)
let filters = await getDataTableFilters(request)

let where = getCollectionAuthWhere(user.id)
let where = getCollectionAuthReadWhere(user.id)

let [count, rawCollections] = await db.$transaction([
db.collection.count({
Expand Down
4 changes: 2 additions & 2 deletions app/routes/app.collections.create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { useLoaderData } from '@remix-run/react'
import { CollectionForm, submitCollectionForm } from '~/forms/CollectionForm'
import { routes } from '~/routes'
import { requireAuthentication } from '~/services/auth.server'
import { getCollectionAuthWhere } from '~/utils/authQueries'
import { getCollectionAuthReadWhere } from '~/utils/authQueries'
import { db } from '~/utils/db.server'

export async function action(args: ActionFunctionArgs) {
Expand All @@ -17,7 +17,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
let user = await requireAuthentication(request)

let catalogs = await db.catalog.findMany({
where: getCollectionAuthWhere(user.id).catalog,
where: getCollectionAuthReadWhere(user.id).catalog,
})

return { catalogs }
Expand Down
9 changes: 6 additions & 3 deletions app/routes/app.items.$itemId.edit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { zx } from 'zodix'
import { z } from 'zod'
import type { AllowedGeometry } from '~/types'
import { prismaToStacItem } from '~/utils/prismaToStac'
import { getCollectionAuthWhere } from '~/utils/authQueries'
import { getCollectionAuthReadWhere } from '~/utils/authQueries'

export const meta: V2_MetaFunction = () => {
return [{ title: 'Edit metadata' }]
Expand All @@ -34,13 +34,16 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
},
},
},
where: getCollectionAuthWhere(user.id),
where: getCollectionAuthReadWhere(user.id),
})

let defaultValues = await db.item.findUnique({
where: {
id: itemId,
collection: getCollectionAuthWhere(user.id),
collection: getCollectionAuthReadWhere(user.id),
},
include: {
assets: true,
},
})

Expand Down
4 changes: 2 additions & 2 deletions app/routes/app.items._index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ import {
} from '~/components/ui/dropdown-menu'
import { routes } from '~/routes'
import { requireAuthentication } from '~/services/auth.server'
import { getCollectionAuthWhere } from '~/utils/authQueries'
import { getCollectionAuthReadWhere } from '~/utils/authQueries'
import { getDataTableFilters } from '~/utils/dataTableFilters'
import { db } from '~/utils/db.server'

export async function loader({ request }: LoaderFunctionArgs) {
let user = await requireAuthentication(request)
let filters = await getDataTableFilters(request)

let whereCollection = getCollectionAuthWhere(user.id)
let whereCollection = getCollectionAuthReadWhere(user.id)

let [count, rawItems] = await db.$transaction([
db.item.count({
Expand Down
4 changes: 2 additions & 2 deletions app/routes/app.items.create.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { redirect } from '@remix-run/node'

import { db } from '~/utils/db.server'
import { ItemForm, submitItemForm } from '~/forms/items/ItemForm'
import { getCollectionAuthWhere } from '~/utils/authQueries'
import { getCollectionAuthContributeWhere } from '~/utils/authQueries'

export const meta: V2_MetaFunction = () => {
return [{ title: 'Register metadata' }]
Expand All @@ -29,7 +29,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
},
},
},
where: getCollectionAuthWhere(user.id),
where: getCollectionAuthContributeWhere(user.id),
})

return { collections }
Expand Down
10 changes: 9 additions & 1 deletion app/routes/app.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,22 @@
import type { LoaderArgs } from '@remix-run/node'
import { Outlet, useRouteLoaderData } from '@remix-run/react'
import { Sidebar } from '~/components/Sidebar'
import type { rootLoader } from '~/root'
import { requireAuthentication } from '~/services/auth.server'

export async function loader({ request }: LoaderArgs) {
await requireAuthentication(request)

return null
}

export default function AppLayout() {
let user = useRouteLoaderData<typeof rootLoader>('root')

return (
<main className="grid grid-cols-5 h-full">
<Sidebar user={user} />
<div className="col-span-3 lg:col-span-4 lg:border-l">
<div className="col-span-4 border-l">
<Outlet />
</div>
</main>
Expand Down
Loading

0 comments on commit 08ea405

Please sign in to comment.