wip:Build devices page

This commit is contained in:
Paul Bienkowski 2023-03-12 11:40:48 +01:00
parent 141460c79f
commit 61b74e90fd
6 changed files with 177 additions and 5 deletions

View file

@ -1,8 +1,8 @@
import logging
from sanic.response import json
from sanic.exceptions import InvalidUsage, Forbidden
from sqlalchemy import select
from sanic.exceptions import InvalidUsage, Forbidden, NotFound
from sqlalchemy import and_, select
from obs.api.app import api, require_auth
from obs.api.db import UserDevice
@ -46,6 +46,32 @@ async def get_user_devices(req):
return json([device.to_dict(req.ctx.user.id) for device in devices])
@api.put("/user/devices/<device_id:int>")
async def put_user_device(req, device_id):
if not req.ctx.user:
raise Forbidden()
body = req.json
query = (
select(UserDevice)
.where(and_(UserDevice.user_id == req.ctx.user.id, UserDevice.id == device_id))
.limit(1)
)
device = (await req.ctx.db.execute(query)).scalar()
if device is None:
raise NotFound()
new_name = body.get("displayName", "").strip()
if new_name and device.display_name != new_name:
device.display_name = new_name
await req.ctx.db.commit()
return json(device.to_dict())
@api.put("/user")
@require_auth
async def put_user(req):

View file

@ -0,0 +1,126 @@
import React, {useCallback, useMemo, useRef} from 'react'
import {useObservable} from 'rxjs-hooks'
import {concat, from, of, Subject} from 'rxjs'
import {Table, Button, Input} from 'semantic-ui-react'
import {useTranslation} from 'react-i18next'
import api from 'api'
import {UserDevice} from 'types'
import {startWith, switchMap} from 'rxjs/operators'
function EditField({value, onEdit}) {
const [editing, setEditing] = React.useState(false)
const [tempValue, setTempValue] = React.useState(value)
const timeoutRef = useRef<null | number>(null)
const cancelTimeout = useCallback(() => {
if (timeoutRef.current != null) {
clearTimeout(timeoutRef.current)
timeoutRef.current = null
}
}, [timeoutRef])
const abort = useCallback(() => {
cancelTimeout()
setEditing(false)
setTempValue(value)
}, [setEditing, setTempValue, value, cancelTimeout])
const confirm = useCallback(() => {
cancelTimeout()
setEditing(false)
onEdit(tempValue)
}, [setEditing, onEdit, tempValue, cancelTimeout])
React.useEffect(() => {
if (value !== tempValue) {
setTempValue(value)
}
}, [value])
if (editing) {
return (
<>
<Input
value={tempValue}
onChange={(e) => setTempValue(e.target.value)}
onBlur={(e) => {
timeoutRef.current = setTimeout(abort, 20)
}}
onKeyPress={(e) => {
if (e.key === 'Enter') {
confirm()
} else if (e.key === 'Escape') {
abort()
}
}}
style={{marginRight: 8}}
/>
<Button icon="check" size="tiny" onClick={confirm} />
<Button icon="repeat" size="tiny" onClick={abort} />
</>
)
} else {
return (
<>
{value && <span style={{marginRight: 8}}>{value}</span>}
<Button icon="edit" size="tiny" onClick={() => setEditing(true)} />
</>
)
}
}
export default function DeviceList() {
const {t} = useTranslation()
const [loading_, setLoading] = React.useState(false)
const trigger$ = useMemo(() => new Subject(), [])
const devices: null | UserDevice[] = useObservable(() =>
trigger$.pipe(
startWith(null),
switchMap(() => concat(of(null), from(api.get('/user/devices'))))
)
)
const setDeviceDisplayName = useCallback(
async (deviceId: number, displayName: string) => {
setLoading(true)
try {
await api.put(`/user/devices/${deviceId}`, {body: {displayName}})
} finally {
setLoading(false)
trigger$.next(null)
}
},
[trigger$, setLoading]
)
const loading = devices == null || loading_
return (
<>
<Table compact {...{loading}}>
<Table.Header>
<Table.Row>
<Table.HeaderCell width={4}>{t('SettingsPage.devices.identifier')}</Table.HeaderCell>
<Table.HeaderCell>{t('SettingsPage.devices.alias')}</Table.HeaderCell>
<Table.HeaderCell />
</Table.Row>
</Table.Header>
<Table.Body>
{devices?.map((device: UserDevice) => (
<Table.Row key={device.id}>
<Table.Cell> {device.identifier}</Table.Cell>
<Table.Cell>
<EditField
value={device.displayName}
onEdit={(displayName: string) => setDeviceDisplayName(device.id, displayName)}
/>
</Table.Cell>
</Table.Row>
))}
</Table.Body>
</Table>
</>
)
}

View file

@ -11,6 +11,7 @@ import api from "api";
import ApiKeySettings from "./ApiKeySettings";
import UserSettingsForm from "./UserSettingsForm";
import DeviceList from "./DeviceList";
const SettingsPage = connect((state) => ({ login: state.login }), { setLogin })(
function SettingsPage({ login, setLogin }) {
@ -54,6 +55,11 @@ const SettingsPage = connect((state) => ({ login: state.login }), { setLogin })(
menuItem: t("SettingsPage.stats.title"),
render: () => <Stats user={login.id} />,
},
{
menuItem: t("SettingsPage.devices.title"),
render: () => <DeviceList />,
},
]}
/>
</Page>

View file

@ -261,6 +261,11 @@ SettingsPage:
stats:
title: Statistik
devices:
title: Geräte
identifier: Bezeichner
alias: Anzeigename
TrackPage:
downloadFailed: Download fehlgeschlagen
downloadError: Diese Fahrt wurde vermutlich nicht korrekt importiert, oder in letzter Zeit nicht aktualisiert. Bitte frage den Administrator um Hilfe mit diesem Problem.

View file

@ -263,6 +263,11 @@ SettingsPage:
stats:
title: Statistics
devices:
title: Devices
identifier: Identifier
alias: Alias
TrackPage:
downloadFailed: Download failed
downloadError: The track probably has not been imported correctly or recently enough. Please ask your administrator for assistance.

View file

@ -236,7 +236,7 @@ SettingsPage:
label: URL d'avatar
apiKey:
title: MA clé d'API
title: Ma clé d'API
description: |
Ici vous trouvez votre clé API, pour l'utilisation dans le OpenBikeSensor.
Vous pouvez la copier et coller dans l'interface de configuration de votre
@ -245,10 +245,10 @@ SettingsPage:
Veuillez protéger votre clé API soigneusement car elle permet un contrôle
total sur votre compte.
urlDescription: |
L'URL de l'API doit être définie comme suit :
L'URL de l'API doit être définie comme suit:
generateDescription: |
Vous pouvez générer une nouvelle clé API ici, qui invalidera l'ancienne,
déconnectant de votre compte tous les appareils sur lesquels vous l'avez utilisée..
déconnectant de votre compte tous les appareils sur lesquels vous l'avez utilisée.
key:
label: Clé API Personnel
@ -260,6 +260,10 @@ SettingsPage:
generate: Générer une nouvelle clé API
devices:
title: Appareils
identifier: Identifiant
alias: Alias
TrackPage:
downloadFailed: Le téléchargement a échoué