Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ MAPBOX_GEOCODING_API="https://api.mapbox.com/geocoding/v5/mapbox.places/"
OSEM_API_URL="https://api.opensensemap.org/"
DIRECTUS_URL="https://coelho.opensensemap.org"
SENSORWIKI_API_URL="https://api.sensors.wiki/"
INGRESS_DOMAIN="ingress.opensensemap.org"

MYBADGES_API_URL = "https://api.v2.mybadges.org/"
MYBADGES_URL = "https://mybadges.org/"
Expand Down
243 changes: 123 additions & 120 deletions app/components/mydevices/dt/columns.tsx
Original file line number Diff line number Diff line change
@@ -1,59 +1,59 @@
"use client";
'use client'

import { type ColumnDef } from "@tanstack/react-table";
import { ArrowUpDown, ClipboardCopy, Ellipsis } from "lucide-react";
import { Button } from "@/components/ui/button";
import { type ColumnDef } from '@tanstack/react-table'
import { ArrowUpDown, ClipboardCopy, Ellipsis } from 'lucide-react'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "~/components/ui/dropdown-menu";
import { type Device } from "~/schema";
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '~/components/ui/dropdown-menu'
import { type Device } from '~/schema'

export type SenseBox = {
id: string;
name: string;
exposure: Device["exposure"];
// model: string;
};
id: string
name: string
exposure: Device['exposure']
// model: string;
}

const colStyle = "pl-0 dark:text-white";
const colStyle = 'pl-0 dark:text-white'

export const columns: ColumnDef<SenseBox>[] = [
{
accessorKey: "name",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
className={colStyle}
>
Name
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
},
{
accessorKey: "exposure",
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
className={colStyle}
>
Exposure
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
);
},
},
/* {
{
accessorKey: 'name',
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className={colStyle}
>
Name
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
},
},
{
accessorKey: 'exposure',
header: ({ column }) => {
return (
<Button
variant="ghost"
onClick={() => column.toggleSorting(column.getIsSorted() === 'asc')}
className={colStyle}
>
Exposure
<ArrowUpDown className="ml-2 h-4 w-4" />
</Button>
)
},
},
/* {
accessorKey: "model",
header: ({ column }) => {
return (
Expand All @@ -68,76 +68,79 @@ export const columns: ColumnDef<SenseBox>[] = [
);
},
}, */
{
accessorKey: "id",
header: () => <div className="dark:text-white pl-0">Sensebox ID</div>,
cell: ({ row }) => {
const senseBox = row.original;
{
accessorKey: 'id',
header: () => <div className="pl-0 dark:text-white">Sensebox ID</div>,
cell: ({ row }) => {
const senseBox = row.original

return (
// <div className="text-right font-medium">
<div className="flex items-center">
<code className="rounded-sm bg-[#f9f2f4] px-1 py-[2px] text-[#c7254e]">
{senseBox?.id}
</code>
<ClipboardCopy
onClick={() => navigator.clipboard.writeText(senseBox?.id)}
className="ml-[6px] mr-1 inline-block h-4 w-4 align-text-bottom text-[#818a91] dark:text-white cursor-pointer"
/>
</div>
);
},
},
{
id: "actions",
header: () => <div className="text-center dark:text-white">Actions</div>,
cell: ({ row }) => {
const senseBox = row.original;
return (
// <div className="text-right font-medium">
<div className="flex items-center">
<code className="rounded-sm bg-[#f9f2f4] px-1 py-[2px] text-[#c7254e]">
{senseBox?.id}
</code>
<ClipboardCopy
onClick={() => navigator.clipboard.writeText(senseBox?.id)}
className="ml-[6px] mr-1 inline-block h-4 w-4 cursor-pointer align-text-bottom text-[#818a91] dark:text-white"
/>
</div>
)
},
},
{
id: 'actions',
header: () => <div className="text-center dark:text-white">Actions</div>,
cell: ({ row }) => {
const senseBox = row.original

return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<Ellipsis className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="dark:bg-dark-background dark:text-dark-text"
>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>
<a href={`/device/${senseBox.id}/overview`}>Overview</a>
</DropdownMenuItem>
<DropdownMenuItem>
<a href={`/explore/${senseBox.id}`}>Show on map</a>
</DropdownMenuItem>
<DropdownMenuItem disabled>
<a href={`/device/${senseBox.id}/edit/general`}>Edit</a>
</DropdownMenuItem>
<DropdownMenuItem disabled>
<a href={`/device/${senseBox.id}/dataupload`}>Data upload</a>
</DropdownMenuItem>
<DropdownMenuItem>
<a
href="https://sensebox.de/de/go-home"
target="_blank"
rel="noopener noreferrer"
>
Support
</a>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(senseBox?.id)}
className="cursor-pointer"
>
Copy ID
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
},
},
];
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<Ellipsis className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="dark:bg-dark-background dark:text-dark-text"
>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuItem>
<a href={`/device/${senseBox.id}/overview`}>Overview</a>
</DropdownMenuItem>
<DropdownMenuItem>
<a href={`/device/${senseBox.id}/script`}>Script</a>
</DropdownMenuItem>
<DropdownMenuItem>
<a href={`/explore/${senseBox.id}`}>Show on map</a>
</DropdownMenuItem>
<DropdownMenuItem disabled>
<a href={`/device/${senseBox.id}/edit/general`}>Edit</a>
</DropdownMenuItem>
<DropdownMenuItem disabled>
<a href={`/device/${senseBox.id}/dataupload`}>Data upload</a>
</DropdownMenuItem>
<DropdownMenuItem>
<a
href="https://sensebox.de/de/go-home"
target="_blank"
rel="noopener noreferrer"
>
Support
</a>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(senseBox?.id)}
className="cursor-pointer"
>
Copy ID
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
},
},
]
113 changes: 113 additions & 0 deletions app/routes/api.boxes.$deviceId.script.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import SketchTemplater from '@sensebox/sketch-templater'

import {
type ActionFunction,
type ActionFunctionArgs,
type LoaderFunction,
type LoaderFunctionArgs,
} from 'react-router'
import { getDevice } from '~/models/device.server'

const cfg = {
'sketch-templater': {
// Ingress domain. Used in the generation of Arduino sketches
// No default
ingress_domain: process.env.INGRESS_DOMAIN || 'ingress.opensensemap.org',
},
}
const templateSketcher = new SketchTemplater(cfg)
type Box = NonNullable<Awaited<ReturnType<typeof getDevice>>>
type BoxForSketch = Box & {
_id: string
sensors: Array<Box['sensors'][number] & { _id: string }>
}

const buildBoxForSketch = (
box: Box,
formEntries: Record<string, FormDataEntryValue>,
): BoxForSketch => ({
...box,
_id: box.id,
sensors: box.sensors.map((sensor) => ({
...sensor,
_id: sensor.id,
})),
...formEntries,
})

const handleSketch = async (
deviceId: string | undefined,
formEntries: Record<string, FormDataEntryValue>,
): Promise<Response> => {
if (deviceId === undefined) {
return Response.json(
{ code: 'Bad Request', message: 'Invalid device id specified' },
{
status: 400,
headers: { 'Content-Type': 'application/json; charset=utf-8' },
},
)
}

const box = await getDevice({ id: deviceId })
if (!box) {
return Response.json(
{ code: 'Not Found', message: 'Device not found' },
{
status: 404,
headers: { 'Content-Type': 'application/json; charset=utf-8' },
},
)
}

const boxForSketch = buildBoxForSketch(box, formEntries)
const encoding = ''
return templateSketcher.generateSketch(boxForSketch, { encoding })
}

export const loader: LoaderFunction = async ({
request,
params,
}: LoaderFunctionArgs): Promise<Response> => {
try {
const url = new URL(request.url)
const formEntries = Object.fromEntries(
url.searchParams.entries(),
) as Record<string, FormDataEntryValue>

return handleSketch(params.deviceId, formEntries)
} catch (err: any) {
return Response.json(
{
code: 'Internal Server Error',
message: err.message || 'An unexpected error occurred',
},
{
status: 500,
headers: { 'Content-Type': 'application/json; charset=utf-8' },
},
)
}
}

export const action: ActionFunction = async ({
request,
params,
}: ActionFunctionArgs): Promise<Response> => {
try {
const formData = await request.formData()
const formEntries = Object.fromEntries(formData.entries())
return handleSketch(params.deviceId, formEntries)
} catch (err: any) {
return Response.json(
{
code: 'Internal Server Error',
message: err.message || 'An unexpected error occurred',
},
{
status: 500,
headers: { 'Content-Type': 'application/json; charset=utf-8' },
},
)
}
}
Loading