wip:Build devices page
This commit is contained in:
parent
141460c79f
commit
61b74e90fd
|
@ -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):
|
||||
|
|
126
frontend/src/pages/SettingsPage/DeviceList.tsx
Normal file
126
frontend/src/pages/SettingsPage/DeviceList.tsx
Normal 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>
|
||||
</>
|
||||
)
|
||||
}
|
|
@ -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>
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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é
|
||||
|
|
Loading…
Reference in a new issue