wip:Build devices page
This commit is contained in:
parent
141460c79f
commit
61b74e90fd
|
@ -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):
|
||||||
|
|
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 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>
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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é
|
||||||
|
|
Loading…
Reference in a new issue