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 import logging
from sanic.response import json from sanic.response import json
from sanic.exceptions import InvalidUsage, Forbidden from sanic.exceptions import InvalidUsage, Forbidden, NotFound
from sqlalchemy import select from sqlalchemy import and_, select
from obs.api.app import api, require_auth from obs.api.app import api, require_auth
from obs.api.db import UserDevice 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]) 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") @api.put("/user")
@require_auth @require_auth
async def put_user(req): 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 ApiKeySettings from "./ApiKeySettings";
import UserSettingsForm from "./UserSettingsForm"; import UserSettingsForm from "./UserSettingsForm";
import DeviceList from "./DeviceList";
const SettingsPage = connect((state) => ({ login: state.login }), { setLogin })( const SettingsPage = connect((state) => ({ login: state.login }), { setLogin })(
function SettingsPage({ login, setLogin }) { function SettingsPage({ login, setLogin }) {
@ -54,6 +55,11 @@ const SettingsPage = connect((state) => ({ login: state.login }), { setLogin })(
menuItem: t("SettingsPage.stats.title"), menuItem: t("SettingsPage.stats.title"),
render: () => <Stats user={login.id} />, render: () => <Stats user={login.id} />,
}, },
{
menuItem: t("SettingsPage.devices.title"),
render: () => <DeviceList />,
},
]} ]}
/> />
</Page> </Page>

View file

@ -261,6 +261,11 @@ SettingsPage:
stats: stats:
title: Statistik title: Statistik
devices:
title: Geräte
identifier: Bezeichner
alias: Anzeigename
TrackPage: TrackPage:
downloadFailed: Download fehlgeschlagen 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. 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: stats:
title: Statistics title: Statistics
devices:
title: Devices
identifier: Identifier
alias: Alias
TrackPage: TrackPage:
downloadFailed: Download failed downloadFailed: Download failed
downloadError: The track probably has not been imported correctly or recently enough. Please ask your administrator for assistance. 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 label: URL d'avatar
apiKey: apiKey:
title: MA clé d'API title: Ma clé d'API
description: | description: |
Ici vous trouvez votre clé API, pour l'utilisation dans le OpenBikeSensor. 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 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 Veuillez protéger votre clé API soigneusement car elle permet un contrôle
total sur votre compte. total sur votre compte.
urlDescription: | urlDescription: |
L'URL de l'API doit être définie comme suit : L'URL de l'API doit être définie comme suit:
generateDescription: | generateDescription: |
Vous pouvez générer une nouvelle clé API ici, qui invalidera l'ancienne, 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: key:
label: Clé API Personnel label: Clé API Personnel
@ -260,6 +260,10 @@ SettingsPage:
generate: Générer une nouvelle clé API generate: Générer une nouvelle clé API
devices:
title: Appareils
identifier: Identifiant
alias: Alias
TrackPage: TrackPage:
downloadFailed: Le téléchargement a échoué downloadFailed: Le téléchargement a échoué