Work on settings page

This commit is contained in:
viktorstrate
2021-06-17 14:42:42 +02:00
parent db792a5a35
commit f80ef5b54c
9 changed files with 179 additions and 165 deletions

View File

@@ -78,7 +78,9 @@ const LoginForm = () => {
// loading={loading || (data && data.authorizeUser.success)}
>
<TextField
className="w-full my-4"
sizeVariant="big"
wrapperClassName="my-4"
className="w-full"
label={t('login_page.field.username', 'Username')}
{...register('username', { required: true })}
error={
@@ -88,7 +90,9 @@ const LoginForm = () => {
}
/>
<TextField
className="w-full my-4"
sizeVariant="big"
wrapperClassName="my-4"
className="w-full"
type="password"
label={t('login_page.field.password', 'Password')}
{...register('password', { required: true })}

View File

@@ -1,14 +1,17 @@
import { gql } from '@apollo/client'
import React, { useRef, useState } from 'react'
import { useMutation, useQuery } from '@apollo/client'
import { Checkbox, Dropdown, Input, Loader } from 'semantic-ui-react'
import { InputLabelDescription, InputLabelTitle } from './SettingsPage'
import { Loader } from 'semantic-ui-react'
import { InputLabelDescription } from './SettingsPage'
import { useTranslation } from 'react-i18next'
import { scanIntervalQuery } from './__generated__/scanIntervalQuery'
import {
changeScanIntervalMutation,
changeScanIntervalMutationVariables,
} from './__generated__/changeScanIntervalMutation'
import Checkbox from '../../primitives/form/Checkbox'
import { TextField } from '../../primitives/form/Input'
import Dropdown, { DropdownItem } from '../../primitives/form/Dropdown'
const SCAN_INTERVAL_QUERY = gql`
query scanIntervalQuery {
@@ -125,13 +128,11 @@ const PeriodicScanner = () => {
},
})
const [
setScanIntervalMutation,
{ loading: scanIntervalMutationLoading },
] = useMutation<
changeScanIntervalMutation,
changeScanIntervalMutationVariables
>(SCAN_INTERVAL_MUTATION)
const [setScanIntervalMutation, { loading: scanIntervalMutationLoading }] =
useMutation<
changeScanIntervalMutation,
changeScanIntervalMutationVariables
>(SCAN_INTERVAL_MUTATION)
const onScanIntervalCheckboxChange = (checked: boolean) => {
setEnablePeriodicScanner(checked)
@@ -154,108 +155,83 @@ const PeriodicScanner = () => {
}
}
type scanIntervalUnitType = {
key: TimeUnit
text: string
value: TimeUnit
}
const scanIntervalUnits: scanIntervalUnitType[] = [
const scanIntervalUnits: DropdownItem[] = [
{
key: TimeUnit.Second,
text: t('settings.periodic_scanner.interval_unit.seconds', 'Seconds'),
label: t('settings.periodic_scanner.interval_unit.seconds', 'Seconds'),
value: TimeUnit.Second,
},
{
key: TimeUnit.Minute,
text: t('settings.periodic_scanner.interval_unit.minutes', 'Minutes'),
label: t('settings.periodic_scanner.interval_unit.minutes', 'Minutes'),
value: TimeUnit.Minute,
},
{
key: TimeUnit.Hour,
text: t('settings.periodic_scanner.interval_unit.hour', 'Hour'),
label: t('settings.periodic_scanner.interval_unit.hour', 'Hour'),
value: TimeUnit.Hour,
},
{
key: TimeUnit.Day,
text: t('settings.periodic_scanner.interval_unit.days', 'Days'),
label: t('settings.periodic_scanner.interval_unit.days', 'Days'),
value: TimeUnit.Day,
},
{
key: TimeUnit.Month,
text: t('settings.periodic_scanner.interval_unit.months', 'Months'),
label: t('settings.periodic_scanner.interval_unit.months', 'Months'),
value: TimeUnit.Month,
},
]
return (
<>
<h3>{t('settings.periodic_scanner.title', 'Periodic scanner')}</h3>
<h3 className="font-semibold text-lg mt-4 mb-2">
{t('settings.periodic_scanner.title', 'Periodic scanner')}
</h3>
<div style={{ margin: '12px 0' }}>
<Checkbox
label={t(
'settings.periodic_scanner.checkbox_label',
'Enable periodic scanner'
)}
disabled={scanIntervalQuery.loading}
checked={enablePeriodicScanner}
onChange={(_, { checked }) =>
onScanIntervalCheckboxChange(checked || false)
}
/>
</div>
<Checkbox
label={t(
'settings.periodic_scanner.checkbox_label',
'Enable periodic scanner'
)}
disabled={scanIntervalQuery.loading}
checked={enablePeriodicScanner}
onChange={event =>
onScanIntervalCheckboxChange(event.target.checked || false)
}
/>
{enablePeriodicScanner && (
<>
<label htmlFor="periodic_scan_field">
<InputLabelTitle>
{t(
'settings.periodic_scanner.field.label',
'Periodic scan interval'
)}
</InputLabelTitle>
<InputLabelDescription>
{t(
'settings.periodic_scanner.field.description',
'How often the scanner should perform automatic scans of all users'
)}
</InputLabelDescription>
</label>
<Input
label={
<Dropdown
onChange={(_, { value }) => {
const newScanInterval: TimeValue = {
...scanInterval,
unit: value as TimeUnit,
}
setScanInterval(newScanInterval)
onScanIntervalUpdate(newScanInterval)
}}
value={scanInterval.unit}
options={scanIntervalUnits}
/>
}
onBlur={() => onScanIntervalUpdate(scanInterval)}
onKeyDown={({ key }: KeyboardEvent) =>
key == 'Enter' && onScanIntervalUpdate(scanInterval)
}
loading={scanIntervalQuery.loading}
labelPosition="right"
style={{ maxWidth: 300 }}
<div className="mt-4">
<label htmlFor="periodic_scan_field">
<h4 className="font-semibold">
{t(
'settings.periodic_scanner.field.label',
'Periodic scan interval'
)}
</h4>
<InputLabelDescription>
{t(
'settings.periodic_scanner.field.description',
'How often the scanner should perform automatic scans of all users'
)}
</InputLabelDescription>
</label>
<div className="flex gap-2">
<TextField
id="periodic_scan_field"
value={scanInterval.value}
onChange={(_, { value }) => {
setScanInterval(x => ({
...x,
value: parseInt(value),
}))
disabled={!enablePeriodicScanner}
/>
<Dropdown
disabled={!enablePeriodicScanner}
items={scanIntervalUnits}
selected={scanInterval.unit}
setSelected={value => {
const newScanInterval: TimeValue = {
...scanInterval,
unit: value as TimeUnit,
}
setScanInterval(newScanInterval)
onScanIntervalUpdate(newScanInterval)
}}
/>
</>
)}
</div>
</div>
<Loader
active={scanIntervalQuery.loading || scanIntervalMutationLoading}
inline

View File

@@ -1,6 +1,6 @@
import React, { useRef, useState } from 'react'
import { useQuery, useMutation, gql } from '@apollo/client'
import { Input, Loader } from 'semantic-ui-react'
// import { Input, Loader } from 'semantic-ui-react'
import { InputLabelTitle, InputLabelDescription } from './SettingsPage'
import { useTranslation } from 'react-i18next'
import { concurrentWorkersQuery } from './__generated__/concurrentWorkersQuery'
@@ -8,6 +8,7 @@ import {
setConcurrentWorkers,
setConcurrentWorkersVariables,
} from './__generated__/setConcurrentWorkers'
import { TextField } from '../../primitives/form/Input'
const CONCURRENT_WORKERS_QUERY = gql`
query concurrentWorkersQuery {
@@ -56,7 +57,7 @@ const ScannerConcurrentWorkers = () => {
}
return (
<div style={{ marginTop: 32 }}>
<div>
<label htmlFor="scanner_concurrent_workers_field">
<InputLabelTitle>
{t('settings.concurrent_workers.title', 'Scanner concurrent workers')}
@@ -68,27 +69,27 @@ const ScannerConcurrentWorkers = () => {
)}
</InputLabelDescription>
</label>
<Input
<TextField
disabled={workerAmountQuery.loading || workersMutationData.loading}
type="number"
min="1"
max="24"
id="scanner_concurrent_workers_field"
value={workerAmount}
onChange={(_, { value }) => {
setWorkerAmount(parseInt(value))
onChange={event => {
setWorkerAmount(parseInt(event.target.value))
}}
onBlur={() => updateWorkerAmount(workerAmount)}
onKeyDown={({ key }: KeyboardEvent) =>
key == 'Enter' && updateWorkerAmount(workerAmount)
onKeyDown={event =>
event.key == 'Enter' && updateWorkerAmount(workerAmount)
}
/>
<Loader
{/* <Loader
active={workerAmountQuery.loading || workersMutationData.loading}
inline
size="small"
style={{ marginLeft: 16 }}
/>
/> */}
</div>
)
}

View File

@@ -1,11 +1,11 @@
import React from 'react'
import { useMutation, gql } from '@apollo/client'
import { Button, Icon } from 'semantic-ui-react'
import PeriodicScanner from './PeriodicScanner'
import ScannerConcurrentWorkers from './ScannerConcurrentWorkers'
import { SectionTitle, InputLabelDescription } from './SettingsPage'
import { useTranslation } from 'react-i18next'
import { scanAllMutation } from './__generated__/scanAllMutation'
import { Button } from '../../primitives/form/Input'
const SCAN_MUTATION = gql`
mutation scanAllMutation {
@@ -32,14 +32,12 @@ const ScannerSection = () => {
)}
</InputLabelDescription>
<Button
icon
labelPosition="left"
className="my-2"
onClick={() => {
startScanner()
}}
disabled={called}
>
<Icon name="sync" />
{t('settings.scanner.scan_all_users', 'Scan all users')}
</Button>
<PeriodicScanner />

View File

@@ -1,32 +1,39 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from 'semantic-ui-react'
import styled from 'styled-components'
import { useIsAdmin } from '../../components/routes/AuthorizedRoute'
import Layout from '../../components/layout/Layout'
import ScannerSection from './ScannerSection'
import UserPreferences from './UserPreferences'
import UsersTable from './Users/UsersTable'
import VersionInfo from './VersionInfo'
import classNames from 'classnames'
export const SectionTitle = styled.h2<{ nospace?: boolean }>`
margin-top: ${({ nospace }) => (nospace ? '0' : '1.4em')} !important;
padding-bottom: 0.3em;
border-bottom: 1px solid #ddd;
`
type SectionTitleProps = {
children: string
nospace?: boolean
}
export const InputLabelTitle = styled.p`
font-size: 1.1em;
font-weight: 600;
margin: 1em 0 0 !important;
`
export const SectionTitle = ({ children, nospace }: SectionTitleProps) => {
return (
<h2
className={classNames(
'pb-1 border-b border-gray-200 text-xl mb-5',
!nospace && 'mt-6'
)}
>
{children}
</h2>
)
}
export const InputLabelDescription = styled.p`
font-size: 0.9em;
margin: 0 0 0.5em !important;
`
export const InputLabelTitle = styled.h3.attrs({
className: 'font-semibold',
})``
export const InputLabelDescription = styled.p.attrs({
className: 'text-sm mb-2',
})``
const SettingsPage = () => {
const { t } = useTranslation()
@@ -41,14 +48,6 @@ const SettingsPage = () => {
<UsersTable />
</>
)}
<Button
style={{ marginTop: 24 }}
onClick={() => {
location.href = '/logout'
}}
>
{t('settings.logout', 'Log out')}
</Button>
<VersionInfo />
</Layout>
)

View File

@@ -2,9 +2,10 @@ import { useMutation, useQuery } from '@apollo/client'
import gql from 'graphql-tag'
import React, { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { Dropdown } from 'semantic-ui-react'
import styled from 'styled-components'
import { LanguageTranslation } from '../../../__generated__/globalTypes'
import Dropdown from '../../primitives/form/Dropdown'
import { Button } from '../../primitives/form/Input'
import {
InputLabelDescription,
InputLabelTitle,
@@ -17,14 +18,14 @@ import {
import { myUserPreferences } from './__generated__/myUserPreferences'
const languagePreferences = [
{ key: 1, text: 'English', flag: 'uk', value: LanguageTranslation.English },
{ key: 2, text: 'Français', flag: 'fr', value: LanguageTranslation.French },
{ key: 3, text: 'Svenska', flag: 'se', value: LanguageTranslation.Swedish },
{ key: 4, text: 'Dansk', flag: 'dk', value: LanguageTranslation.Danish },
{ key: 5, text: 'Español', flag: 'es', value: LanguageTranslation.Spanish },
{ key: 6, text: 'polski', flag: 'pl', value: LanguageTranslation.Polish },
{ key: 7, text: 'Italiano', flag: 'it', value: LanguageTranslation.Italian },
{ key: 8, text: 'Deutsch', flag: 'de', value: LanguageTranslation.German },
{ key: 1, label: 'English', flag: 'uk', value: LanguageTranslation.English },
{ key: 2, label: 'Français', flag: 'fr', value: LanguageTranslation.French },
{ key: 3, label: 'Svenska', flag: 'se', value: LanguageTranslation.Swedish },
{ key: 4, label: 'Dansk', flag: 'dk', value: LanguageTranslation.Danish },
{ key: 5, label: 'Español', flag: 'es', value: LanguageTranslation.Spanish },
{ key: 6, label: 'polski', flag: 'pl', value: LanguageTranslation.Polish },
{ key: 7, label: 'Italiano', flag: 'it', value: LanguageTranslation.Italian },
{ key: 8, label: 'Deutsch', flag: 'de', value: LanguageTranslation.German },
]
const CHANGE_USER_PREFERENCES = gql`
@@ -45,6 +46,21 @@ const MY_USER_PREFERENCES = gql`
}
`
const LogoutButton = () => {
const { t } = useTranslation()
return (
<Button
className="mb-4"
onClick={() => {
location.href = '/logout'
}}
>
{t('settings.logout', 'Log out')}
</Button>
)
}
const UserPreferencesWrapper = styled.div`
margin-bottom: 24px;
`
@@ -60,7 +76,7 @@ const UserPreferences = () => {
>(CHANGE_USER_PREFERENCES)
const sortedLanguagePrefs = useMemo(
() => languagePreferences.sort((a, b) => a.text.localeCompare(b.text)),
() => languagePreferences.sort((a, b) => a.label.localeCompare(b.label)),
[]
)
@@ -73,6 +89,7 @@ const UserPreferences = () => {
<SectionTitle nospace>
{t('settings.user_preferences.title', 'User preferences')}
</SectionTitle>
<LogoutButton />
<label id="user_pref_change_language_field">
<InputLabelTitle>
{t(
@@ -93,19 +110,18 @@ const UserPreferences = () => {
'settings.user_preferences.language_selector.placeholder',
'Select language'
)}
clearable
options={sortedLanguagePrefs}
onChange={(event, { value: language }) => {
items={sortedLanguagePrefs}
setSelected={language => {
changePrefs({
variables: {
language: language as LanguageTranslation,
},
})
}}
selection
search
value={data?.myUserPreferences.language || undefined}
loading={loadingPrefs}
// selection
// search
selected={data?.myUserPreferences.language || undefined}
// loading={loadingPrefs}
disabled={loadingPrefs}
/>
</UserPreferencesWrapper>

View File

@@ -202,7 +202,6 @@ const MorePopoverSectionPassword = ({
disabled={!activated}
type={passwordHidden ? 'password' : 'text'}
value={passwordInputValue}
sizeVariant="small"
className="mt-2"
onKeyDown={event => {
if (
@@ -252,7 +251,7 @@ const MorePopover = ({ id, share, query }: MorePopoverProps) => {
<MorePopoverSectionPassword id={id} share={share} query={query} />
<div className="px-4 py-2 border-t border-gray-200 mt-2 mb-2">
<Checkbox label="Expiration date" />
<TextField sizeVariant="small" className="mt-2" />
<TextField className="mt-2" />
</div>
</ArrowPopoverPanel>
</Popover.Panel>

View File

@@ -1,3 +1,4 @@
import classNames from 'classnames'
import React from 'react'
import styled from 'styled-components'
@@ -9,7 +10,7 @@ const DropdownStyledSelect = styled.select`
background-position: center right 10px;
`
type DropdownItem = {
export type DropdownItem = {
value: string
label: string
}
@@ -17,13 +18,14 @@ type DropdownItem = {
type DropdownProps = React.SelectHTMLAttributes<HTMLSelectElement> & {
items: DropdownItem[]
selected?: string
setSelected(label: string): void
setSelected(value: string): void
}
const Dropdown = ({
items,
selected,
setSelected,
className,
...otherProps
}: DropdownProps) => {
const onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
@@ -39,7 +41,10 @@ const Dropdown = ({
return (
<DropdownStyledSelect
className="bg-gray-50 px-2 py-0.5 pr-6 rounded border border-gray-200 focus:outline-none focus:border-blue-300 text-[#222] hover:bg-gray-100"
className={classNames(
'bg-gray-50 px-2 py-0.5 pr-6 rounded border border-gray-200 focus:outline-none focus:border-blue-300 text-[#222] hover:bg-gray-100 disabled:hover:bg-gray-50 disabled:text-gray-500 disabled:cursor-default',
className
)}
value={selected}
onChange={onChange}
{...otherProps}

View File

@@ -8,7 +8,8 @@ type TextFieldProps = {
label?: string
error?: string
className?: ClassNamesArg
sizeVariant?: 'default' | 'small'
wrapperClassName?: ClassNamesArg
sizeVariant?: 'default' | 'big'
action?: () => void
loading?: boolean
} & Omit<React.InputHTMLAttributes<HTMLInputElement>, 'className'>
@@ -19,6 +20,7 @@ export const TextField = forwardRef(
label,
error,
className,
wrapperClassName,
sizeVariant,
action,
loading,
@@ -52,9 +54,10 @@ export const TextField = forwardRef(
<input
onKeyUp={keyUpEvent}
className={classNames(
'block border rounded-md w-full focus:ring-2 focus:outline-none px-2',
'block border rounded-md focus:ring-2 focus:outline-none px-2',
variant,
sizeVariant == 'default' ? 'py-2' : 'py-1'
sizeVariant == 'big' ? 'py-2' : 'py-1',
className
)}
{...inputProps}
ref={ref}
@@ -79,8 +82,7 @@ export const TextField = forwardRef(
disabled={disabled}
aria-label="Submit"
className={classNames(
'absolute top-[1px] right-0 p-2',
disabled ? 'text-gray-400 cursor-default' : 'text-gray-600'
'absolute top-[1px] right-0 p-2 text-gray-600 disabled:text-gray-400 disabled:cursor-default'
)}
onClick={() => action()}
>
@@ -94,8 +96,8 @@ export const TextField = forwardRef(
if (error) errorElm = <div className="text-red-800">{error}</div>
const wrapperClasses = classNames(
className,
sizeVariant == 'small' && 'text-sm'
sizeVariant == 'default' && 'text-sm',
wrapperClassName
)
if (label) {
@@ -119,12 +121,26 @@ export const TextField = forwardRef(
}
)
export const Submit = (props: React.InputHTMLAttributes<HTMLInputElement>) => {
return (
<input
className={`rounded-md px-8 py-2 focus:outline-none hover:cursor-pointer bg-[#eee] hover:bg-[#e6e6e6] focus:bg-[#e6e6e6] ${props.className}`}
type="submit"
{...props}
/>
)
}
const buttonStyles =
'bg-gray-50 px-6 py-0.5 rounded border border-gray-200 focus:outline-none focus:border-blue-300 text-[#222] hover:bg-gray-100'
export const Submit = ({
className,
...props
}: React.InputHTMLAttributes<HTMLInputElement>) => (
<input
className={classNames(buttonStyles, className)}
type="submit"
{...props}
/>
)
export const Button = ({
children,
className,
...props
}: React.ButtonHTMLAttributes<HTMLButtonElement>) => (
<button className={classNames(buttonStyles, className)} {...props}>
{children}
</button>
)