Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
21 changes: 21 additions & 0 deletions src/api/devices.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,27 @@ export const unpairDevice = async (dongleId: string) =>
method: 'POST',
})

export const grantDeviceReadPermission = async (dongleId: string, email: string) =>
fetcher<{ success: number }>(`/v1/devices/${dongleId}/add_user`, {
method: 'POST',
body: JSON.stringify({ email: email }),
headers: {
'Content-Type': 'application/json',
},
})

export const removeDeviceReadPermission = async (dongleId: string, email: string) =>
fetcher<{ success: number }>(`/v1/devices/${dongleId}/del_user`, {
method: 'POST',
body: JSON.stringify({ email: email }),
headers: {
'Content-Type': 'application/json',
},
})

export const getDeviceUsers = async (dongleId: string): Promise<{ email: string; permission: string }[]> =>
fetcher<{ email: string; permission: string }[]>(`/v1/devices/${dongleId}/users`)

const validatePairToken = (
input: string,
): {
Expand Down
2 changes: 1 addition & 1 deletion src/components/material/Icon.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export const Icons = [
'add', 'arrow_back', 'camera', 'check', 'chevron_right', 'clear', 'close', 'delete', 'description', 'directions_car', 'download', 'error',
'file_copy', 'flag', 'info', 'keyboard_arrow_down', 'keyboard_arrow_up', 'local_fire_department', 'logout', 'menu', 'my_location',
'open_in_new', 'payments', 'person', 'progress_activity', 'satellite_alt', 'search', 'settings', 'upload', 'videocam', 'refresh',
'login', 'person_off', 'autorenew', 'close_small', 'pause', 'play_arrow', 'clear_all',
'login', 'person_off', 'autorenew', 'close_small', 'pause', 'play_arrow', 'clear_all', 'share'
] as const

export type IconName = (typeof Icons)[number]
Expand Down
19 changes: 8 additions & 11 deletions src/components/material/TextField.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Show, createEffect, createSignal, splitProps, type Component, type JSX } from 'solid-js'
import { Show, createSignal, splitProps, type Component, type JSX } from 'solid-js'
import clsx from 'clsx'

type TextFieldProps = {
Expand All @@ -7,7 +7,8 @@ type TextFieldProps = {
helperText?: string
error?: string
value?: string
} & Omit<JSX.InputHTMLAttributes<HTMLInputElement>, 'class'>
onInput?: (value: string) => void
} & Omit<JSX.InputHTMLAttributes<HTMLInputElement>, 'class' | 'onInput'>

const stateColors = {
base: {
Expand Down Expand Up @@ -40,14 +41,8 @@ const TextField: Component<TextFieldProps> = (props) => {

const [focused, setFocused] = createSignal(false)
const [hovered, setHovered] = createSignal(false)
const [inputValue, setInputValue] = createSignal(props.value || '')

// Keep local value in sync with prop value
createEffect(() => {
if (props.value) setInputValue(props.value)
})

const labelFloating = () => focused() || inputValue()?.length > 0
const labelFloating = () => focused() || (props.value?.length || 0) > 0

const getStateStyle = () => {
const state = { ...stateColors.base }
Expand Down Expand Up @@ -100,8 +95,10 @@ const TextField: Component<TextFieldProps> = (props) => {
getStateStyle().input,
props.label && labelFloating() && 'pt-6 pb-2',
)}
value={inputValue()}
onInput={(e) => setInputValue(e.target.value)}
value={props.value}
onInput={(e) => {
if (props.onInput) props.onInput(e.target.value)
}}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
/>
Expand Down
51 changes: 49 additions & 2 deletions src/pages/dashboard/activities/SettingsActivity.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { Accessor, VoidComponent, Setter, ParentComponent, Resource, JSXEle
import { useLocation } from '@solidjs/router'
import clsx from 'clsx'

import { getDevice, unpairDevice } from '~/api/devices'
import { getDevice, getDeviceUsers, grantDeviceReadPermission, unpairDevice, removeDeviceReadPermission } from '~/api/devices'
import {
cancelSubscription,
getStripeCheckout,
Expand All @@ -22,6 +22,7 @@ import IconButton from '~/components/material/IconButton'
import TopAppBar from '~/components/material/TopAppBar'
import { createQuery } from '~/utils/createQuery'
import { getDeviceName } from '~/utils/device'
import TextField from '~/components/material/TextField'

const useAction = <T,>(action: () => Promise<T>): [() => void, Resource<T>] => {
const [source, setSource] = createSignal(false)
Expand Down Expand Up @@ -401,15 +402,61 @@ const PrimeManage: VoidComponent<{ dongleId: string }> = (props) => {

const DeviceSettingsForm: VoidComponent<{ dongleId: string; device: Resource<Device> }> = (props) => {
const [deviceName] = createResource(props.device, getDeviceName)

const [deviceUsers, { refetch: refetchDeviceUsers }] = createResource(props.dongleId, getDeviceUsers)
const [shareEmail, setShareEmail] = createSignal('')
const [unpair, unpairData] = useAction(async () => {
const { success } = await unpairDevice(props.dongleId)
if (success) window.location.href = window.location.origin
})
const [share, shareData] = useAction(async () => {
const { success } = await grantDeviceReadPermission(props.dongleId, shareEmail())
if (success) refetchDeviceUsers()
setShareEmail('')
})

const [unshareLoading, setUnshareLoading] = createSignal(false)

const unshare = async (email: string) => {
setUnshareLoading(true)
const { success } = await removeDeviceReadPermission(props.dongleId, email)
if (success) refetchDeviceUsers()
setUnshareLoading(false)
}

return (
<div class="flex flex-col gap-4">
<h2 class="text-lg">{deviceName()}</h2>
<Show when={props.device()?.is_owner}>
<div class="flex flex-col gap-2">
<h3 class="text-md">{(deviceUsers() || []).length - 1 > 0 ? 'shared with:' : 'share device'}</h3>
<For each={deviceUsers()} fallback={<div>loading</div>}>
{(user, _index) => (
<Show when={user.permission !== 'owner'}>
<div class="flex items-center gap-2 justify-between">
<div>{user.email}</div>
<Button color="error" onClick={() => unshare(user.email)} loading={unshareLoading()}>
<Icon name="delete" />
</Button>
</div>
</Show>
)}
</For>
<div class="flex items-center gap-2 justify-between">
<TextField
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make the text field fill the entire space instead of leaving a gap between it and the share button

label="email"
id="email-box"
value={shareEmail()}
onInput={(val: string) => {
setShareEmail(val)
}}
/>
<Button color="secondary" onClick={share} loading={shareData.loading}>
<Icon name="share" />
</Button>
</div>
</div>
</Show>

<Show when={unpairData.error}>
<div class="flex gap-2 rounded-sm bg-surface-container-high p-2 text-sm text-on-surface">
<Icon class="text-error" name="error" size="20" />
Expand Down