Add admin setting to change max concurrent workers

This commit is contained in:
viktorstrate
2020-09-21 21:32:30 +02:00
parent a4f53cbc35
commit 9c9807ecc9
14 changed files with 568 additions and 257 deletions

View File

@@ -0,0 +1,4 @@
ALTER TABLE site_info
DROP COLUMN IF EXISTS periodic_scan_interval,
DrOP COLUMN IF EXISTS concurrent_workers;

View File

@@ -1,3 +1,4 @@
ALTER TABLE site_info
ADD COLUMN IF NOT EXISTS periodic_scan_interval int(8) NOT NULL DEFAULT 0;
ADD COLUMN IF NOT EXISTS periodic_scan_interval int(8) NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS concurrent_workers int(8) NOT NULL DEFAULT 3;

View File

@@ -125,6 +125,7 @@ type ComplexityRoot struct {
ScanAll func(childComplexity int) int
ScanUser func(childComplexity int, userID int) int
SetPeriodicScanInterval func(childComplexity int, interval int) int
SetScannerConcurrentWorkers func(childComplexity int, workers int) int
ShareAlbum func(childComplexity int, albumID int, expire *time.Time, password *string) int
ShareMedia func(childComplexity int, mediaID int, expire *time.Time, password *string) int
UpdateUser func(childComplexity int, id int, username *string, rootPath *string, password *string, admin *bool) int
@@ -178,6 +179,7 @@ type ComplexityRoot struct {
}
SiteInfo struct {
ConcurrentWorkers func(childComplexity int) int
InitialSetup func(childComplexity int) int
PeriodicScanInterval func(childComplexity int) int
}
@@ -243,6 +245,7 @@ type MutationResolver interface {
CreateUser(ctx context.Context, username string, rootPath string, password *string, admin bool) (*models.User, error)
DeleteUser(ctx context.Context, id int) (*models.User, error)
SetPeriodicScanInterval(ctx context.Context, interval int) (int, error)
SetScannerConcurrentWorkers(ctx context.Context, workers int) (int, error)
}
type QueryResolver interface {
SiteInfo(ctx context.Context) (*models.SiteInfo, error)
@@ -727,6 +730,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.Mutation.SetPeriodicScanInterval(childComplexity, args["interval"].(int)), true
case "Mutation.setScannerConcurrentWorkers":
if e.complexity.Mutation.SetScannerConcurrentWorkers == nil {
break
}
args, err := ec.field_Mutation_setScannerConcurrentWorkers_args(context.TODO(), rawArgs)
if err != nil {
return 0, false
}
return e.complexity.Mutation.SetScannerConcurrentWorkers(childComplexity, args["workers"].(int)), true
case "Mutation.shareAlbum":
if e.complexity.Mutation.ShareAlbum == nil {
break
@@ -1027,6 +1042,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in
return e.complexity.ShareToken.Token(childComplexity), true
case "SiteInfo.concurrentWorkers":
if e.complexity.SiteInfo.ConcurrentWorkers == nil {
break
}
return e.complexity.SiteInfo.ConcurrentWorkers(childComplexity), true
case "SiteInfo.initialSetup":
if e.complexity.SiteInfo.InitialSetup == nil {
break
@@ -1327,6 +1349,9 @@ type Mutation {
a value of 0 will disable periodic scans
"""
setPeriodicScanInterval(interval: Int!): Int!
"Set max number of concurrent scanner jobs running at once"
setScannerConcurrentWorkers(workers: Int!): Int!
}
type Subscription {
@@ -1387,6 +1412,8 @@ type SiteInfo {
initialSetup: Boolean!
"How often automatic scans should be initiated in seconds"
periodicScanInterval: Int! @isAdmin
"How many max concurrent scanner jobs that should run at once"
concurrentWorkers: Int! @isAdmin
}
type User {
@@ -1783,6 +1810,21 @@ func (ec *executionContext) field_Mutation_setPeriodicScanInterval_args(ctx cont
return args, nil
}
func (ec *executionContext) field_Mutation_setScannerConcurrentWorkers_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
var arg0 int
if tmp, ok := rawArgs["workers"]; ok {
ctx := graphql.WithFieldInputContext(ctx, graphql.NewFieldInputWithField("workers"))
arg0, err = ec.unmarshalNInt2int(ctx, tmp)
if err != nil {
return nil, err
}
}
args["workers"] = arg0
return args, nil
}
func (ec *executionContext) field_Mutation_shareAlbum_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
var err error
args := map[string]interface{}{}
@@ -4203,6 +4245,47 @@ func (ec *executionContext) _Mutation_setPeriodicScanInterval(ctx context.Contex
return ec.marshalNInt2int(ctx, field.Selections, res)
}
func (ec *executionContext) _Mutation_setScannerConcurrentWorkers(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "Mutation",
Field: field,
Args: nil,
IsMethod: true,
}
ctx = graphql.WithFieldContext(ctx, fc)
rawArgs := field.ArgumentMap(ec.Variables)
args, err := ec.field_Mutation_setScannerConcurrentWorkers_args(ctx, rawArgs)
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
fc.Args = args
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return ec.resolvers.Mutation().SetScannerConcurrentWorkers(rctx, args["workers"].(int))
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(int)
fc.Result = res
return ec.marshalNInt2int(ctx, field.Selections, res)
}
func (ec *executionContext) _Notification_key(ctx context.Context, field graphql.CollectedField, obj *models.Notification) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@@ -5503,6 +5586,60 @@ func (ec *executionContext) _SiteInfo_periodicScanInterval(ctx context.Context,
return ec.marshalNInt2int(ctx, field.Selections, res)
}
func (ec *executionContext) _SiteInfo_concurrentWorkers(ctx context.Context, field graphql.CollectedField, obj *models.SiteInfo) (ret graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
ec.Error(ctx, ec.Recover(ctx, r))
ret = graphql.Null
}
}()
fc := &graphql.FieldContext{
Object: "SiteInfo",
Field: field,
Args: nil,
IsMethod: false,
}
ctx = graphql.WithFieldContext(ctx, fc)
resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
directive0 := func(rctx context.Context) (interface{}, error) {
ctx = rctx // use context from middleware stack in children
return obj.ConcurrentWorkers, nil
}
directive1 := func(ctx context.Context) (interface{}, error) {
if ec.directives.IsAdmin == nil {
return nil, errors.New("directive isAdmin is not implemented")
}
return ec.directives.IsAdmin(ctx, obj, directive0)
}
tmp, err := directive1(rctx)
if err != nil {
return nil, err
}
if tmp == nil {
return nil, nil
}
if data, ok := tmp.(int); ok {
return data, nil
}
return nil, fmt.Errorf(`unexpected type %T from directive, should be int`, tmp)
})
if err != nil {
ec.Error(ctx, err)
return graphql.Null
}
if resTmp == nil {
if !graphql.HasFieldError(ctx, fc) {
ec.Errorf(ctx, "must not be null")
}
return graphql.Null
}
res := resTmp.(int)
fc.Result = res
return ec.marshalNInt2int(ctx, field.Selections, res)
}
func (ec *executionContext) _Subscription_notification(ctx context.Context, field graphql.CollectedField) (ret func() graphql.Marshaler) {
defer func() {
if r := recover(); r != nil {
@@ -7626,6 +7763,11 @@ func (ec *executionContext) _Mutation(ctx context.Context, sel ast.SelectionSet)
if out.Values[i] == graphql.Null {
invalids++
}
case "setScannerConcurrentWorkers":
out.Values[i] = ec._Mutation_setScannerConcurrentWorkers(ctx, field)
if out.Values[i] == graphql.Null {
invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}
@@ -8041,6 +8183,11 @@ func (ec *executionContext) _SiteInfo(ctx context.Context, sel ast.SelectionSet,
if out.Values[i] == graphql.Null {
invalids++
}
case "concurrentWorkers":
out.Values[i] = ec._SiteInfo_concurrentWorkers(ctx, field, obj)
if out.Values[i] == graphql.Null {
invalids++
}
default:
panic("unknown field " + strconv.Quote(field.Name))
}

View File

@@ -56,6 +56,8 @@ type SiteInfo struct {
InitialSetup bool `json:"initialSetup"`
// How often automatic scans should be initiated in seconds
PeriodicScanInterval int `json:"periodicScanInterval"`
// How many max concurrent scanner jobs that should run at once
ConcurrentWorkers int `json:"concurrentWorkers"`
}
type MediaType string

View File

@@ -20,7 +20,8 @@ func GetSiteInfo(db *sql.DB) (*SiteInfo, error) {
}
var initialSetup bool
var periodicScanInterval int = 0
var periodicScanInterval int
var concurrentWorkers int
if !rows.Next() {
// Entry does not exist
@@ -29,7 +30,7 @@ func GetSiteInfo(db *sql.DB) (*SiteInfo, error) {
}
initialSetup = true
} else {
if err := rows.Scan(&initialSetup, &periodicScanInterval); err != nil {
if err := rows.Scan(&initialSetup, &periodicScanInterval, &concurrentWorkers); err != nil {
return nil, err
}
}
@@ -37,5 +38,6 @@ func GetSiteInfo(db *sql.DB) (*SiteInfo, error) {
return &SiteInfo{
InitialSetup: initialSetup,
PeriodicScanInterval: periodicScanInterval,
ConcurrentWorkers: concurrentWorkers,
}, nil
}

View File

@@ -62,3 +62,25 @@ func (r *mutationResolver) SetPeriodicScanInterval(ctx context.Context, interval
return dbInterval, nil
}
func (r *mutationResolver) SetScannerConcurrentWorkers(ctx context.Context, workers int) (int, error) {
if workers < 0 {
return 0, errors.New("concurrent workers must be positive")
}
_, err := r.Database.Exec("UPDATE site_info SET concurrent_workers = ?", workers)
if err != nil {
return 0, err
}
var dbWorkers int
row := r.Database.QueryRow("SELECT concurrent_workers FROM site_info")
if err = row.Scan(&dbWorkers); err != nil {
return 0, err
}
scanner.ChangeScannerConcurrentWorkers(dbWorkers)
return dbWorkers, nil
}

View File

@@ -98,6 +98,9 @@ type Mutation {
a value of 0 will disable periodic scans
"""
setPeriodicScanInterval(interval: Int!): Int!
"Set max number of concurrent scanner jobs running at once"
setScannerConcurrentWorkers(workers: Int!): Int!
}
type Subscription {
@@ -158,6 +161,8 @@ type SiteInfo {
initialSetup: Boolean!
"How often automatic scans should be initiated in seconds"
periodicScanInterval: Int! @isAdmin
"How many max concurrent scanner jobs that should run at once"
concurrentWorkers: Int! @isAdmin
}
type User {

View File

@@ -37,16 +37,33 @@ type ScannerQueue struct {
var global_scanner_queue ScannerQueue
func InitializeScannerQueue(db *sql.DB) {
func InitializeScannerQueue(db *sql.DB) error {
var concurrentWorkers int
row := db.QueryRow("SELECT concurrent_workers FROM site_info")
if err := row.Scan(&concurrentWorkers); err != nil {
return errors.Wrap(err, "get current workers from database")
}
global_scanner_queue = ScannerQueue{
idle_chan: make(chan bool, 1),
in_progress: make([]ScannerJob, 0),
up_next: make([]ScannerJob, 0),
db: db,
settings: ScannerQueueSettings{max_concurrent_tasks: 3},
settings: ScannerQueueSettings{max_concurrent_tasks: concurrentWorkers},
}
go global_scanner_queue.startBackgroundWorker()
return nil
}
func ChangeScannerConcurrentWorkers(newMaxWorkers int) {
global_scanner_queue.mutex.Lock()
defer global_scanner_queue.mutex.Unlock()
log.Printf("Scanner max concurrent workers changed to: %d", newMaxWorkers)
global_scanner_queue.settings.max_concurrent_tasks = newMaxWorkers
}
func (queue *ScannerQueue) startBackgroundWorker() {

View File

@@ -41,8 +41,13 @@ func main() {
log.Panicf("Could not migrate database: %s\n", err)
}
scanner.InitializeScannerQueue(db)
scanner.InitializePeriodicScanner(db)
if err := scanner.InitializeScannerQueue(db); err != nil {
log.Panicf("Could not initialize scanner queue: %s\n", err)
}
if err := scanner.InitializePeriodicScanner(db); err != nil {
log.Panicf("Could not initialize periodic scanner: %s", err)
}
rootRouter := mux.NewRouter()

View File

@@ -0,0 +1,237 @@
import React, { useRef, useState } from 'react'
import {
Button,
Checkbox,
Dropdown,
Icon,
Input,
Loader,
} from 'semantic-ui-react'
import { useMutation, useQuery } from 'react-apollo'
import gql from 'graphql-tag'
import styled from 'styled-components'
import {
SectionTitle,
InputLabelTitle,
InputLabelDescription,
} from './SettingsPage'
const SCAN_INTERVAL_QUERY = gql`
query scanIntervalQuery {
siteInfo {
periodicScanInterval
}
}
`
const SCAN_INTERVAL_MUTATION = gql`
mutation changeScanIntervalMutation($interval: Int!) {
setPeriodicScanInterval(interval: $interval)
}
`
const timeUnits = [
{
value: 'second',
multiplier: 1,
},
{
value: 'minute',
multiplier: 60,
},
{
value: 'hour',
multiplier: 60 * 60,
},
{
value: 'day',
multiplier: 60 * 60 * 24,
},
{
value: 'month',
multiplier: 60 * 60 * 24 * 30,
},
]
const convertToSeconds = ({ value, unit }) => {
return parseInt(value * timeUnits.find(x => x.value == unit).multiplier)
}
const convertToAppropriateUnit = ({ value, unit }) => {
if (value == 0) {
return {
unit: 'second',
value: 0,
}
}
const seconds = convertToSeconds({ value, unit })
let resultingUnit = timeUnits.first
for (const unit of timeUnits) {
if (
seconds / unit.multiplier >= 1 &&
(seconds / unit.multiplier) % 1 == 0
) {
resultingUnit = unit
} else {
break
}
}
return {
unit: resultingUnit.value,
value: seconds / resultingUnit.multiplier,
}
}
const PeriodicScanner = () => {
const [enablePeriodicScanner, setEnablePeriodicScanner] = useState(false)
const [scanInterval, setScanInterval] = useState({
value: 4,
unit: 'minute',
})
const scanIntervalServerValue = useRef(null)
const scanIntervalQuery = useQuery(SCAN_INTERVAL_QUERY, {
onCompleted(data) {
const queryScanInterval = data.siteInfo.periodicScanInterval
if (queryScanInterval == 0) {
setScanInterval({
unit: 'second',
value: '',
})
} else {
setScanInterval(
convertToAppropriateUnit({
unit: 'second',
value: queryScanInterval,
})
)
}
setEnablePeriodicScanner(queryScanInterval > 0)
},
})
const [
setScanIntervalMutation,
{ loading: scanIntervalMutationLoading },
] = useMutation(SCAN_INTERVAL_MUTATION)
const onScanIntervalCheckboxChange = checked => {
setEnablePeriodicScanner(checked)
onScanIntervalUpdate(checked ? scanInterval : { value: 0, unit: 'second' })
}
const onScanIntervalUpdate = scanInterval => {
const seconds = convertToSeconds(scanInterval)
if (scanIntervalServerValue.current != seconds) {
setScanIntervalMutation({
variables: {
interval: convertToSeconds(scanInterval),
},
})
scanIntervalServerValue.current = seconds
}
}
const scanIntervalUnits = [
{
key: 'second',
text: 'Seconds',
value: 'second',
},
{
key: 'minute',
text: 'Minutes',
value: 'minute',
},
{
key: 'hour',
text: 'Hours',
value: 'hour',
},
{
key: 'day',
text: 'Days',
value: 'day',
},
{
key: 'month',
text: 'Months',
value: 'month',
},
]
return (
<>
<h3>Periodic scanner</h3>
<div style={{ margin: '12px 0' }}>
<Checkbox
label="Enable periodic scanner"
disabled={scanIntervalQuery.loading}
checked={enablePeriodicScanner}
onChange={(_, { checked }) => onScanIntervalCheckboxChange(checked)}
/>
</div>
{enablePeriodicScanner && (
<>
<label htmlFor="periodic_scan_field">
<InputLabelTitle>Periodic scan interval</InputLabelTitle>
<InputLabelDescription>
How often the scanner should perform automatic scans of all users
</InputLabelDescription>
</label>
<Input
label={
<Dropdown
onChange={(_, { value }) => {
const newScanInterval = {
...scanInterval,
unit: value,
}
setScanInterval(newScanInterval)
onScanIntervalUpdate(newScanInterval)
}}
value={scanInterval.unit}
options={scanIntervalUnits}
/>
}
onBlur={() => onScanIntervalUpdate(scanInterval)}
onKeyDown={({ key }) =>
key == 'Enter' && onScanIntervalUpdate(scanInterval)
}
loading={scanIntervalQuery.loading}
labelPosition="right"
style={{ maxWidth: 300 }}
id="periodic_scan_field"
value={scanInterval.value}
onChange={(_, { value }) => {
setScanInterval(x => ({
...x,
value,
}))
}}
/>
</>
)}
<Loader
active={scanIntervalQuery.loading || scanIntervalMutationLoading}
inline
size="small"
style={{ marginLeft: 16 }}
/>
</>
)
}
export default PeriodicScanner

View File

@@ -0,0 +1,80 @@
import gql from 'graphql-tag'
import React, { useRef, useState } from 'react'
import { useQuery, useMutation } from 'react-apollo'
import { Input, Loader } from 'semantic-ui-react'
import { InputLabelTitle, InputLabelDescription } from './SettingsPage'
const CONCURRENT_WORKERS_QUERY = gql`
query concurrentWorkersQuery {
siteInfo {
concurrentWorkers
}
}
`
const SET_CONCURRENT_WORKERS_MUTATION = gql`
mutation setConcurrentWorkers($workers: Int!) {
setScannerConcurrentWorkers(workers: $workers)
}
`
const ScannerConcurrentWorkers = () => {
const workerAmountQuery = useQuery(CONCURRENT_WORKERS_QUERY, {
onCompleted(data) {
setWorkerAmount(data.siteInfo.concurrentWorkers)
workerAmountServerValue.current = data.siteInfo.concurrentWorkers
},
})
const [setWorkersMutation, workersMutationData] = useMutation(
SET_CONCURRENT_WORKERS_MUTATION
)
const workerAmountServerValue = useRef(null)
const [workerAmount, setWorkerAmount] = useState('')
const updateWorkerAmount = workerAmount => {
if (workerAmountServerValue.current != workerAmount) {
workerAmountServerValue.current = workerAmount
setWorkersMutation({
variables: {
workers: workerAmount,
},
})
}
}
return (
<div style={{ marginTop: 32 }}>
<label htmlFor="scanner_concurrent_workers_field">
<InputLabelTitle>Scanner Concurrent Workers</InputLabelTitle>
<InputLabelDescription>
The maximum amount of scanner jobs that is allowed to run at once
</InputLabelDescription>
</label>
<Input
disabled={workerAmountQuery.loading || workersMutationData.loading}
type="number"
min="1"
max="24"
id="scanner_concurrent_workers_field"
value={workerAmount}
onChange={(_, { value }) => {
setWorkerAmount(value)
}}
onBlur={() => updateWorkerAmount(workerAmount)}
onKeyDown={({ key }) =>
key == 'Enter' && updateWorkerAmount(workerAmount)
}
/>
<Loader
active={workerAmountQuery.loading || workersMutationData.loading}
inline
size="small"
style={{ marginLeft: 16 }}
/>
</div>
)
}
export default ScannerConcurrentWorkers

View File

@@ -1,17 +1,10 @@
import React, { useRef, useState } from 'react'
import {
Button,
Checkbox,
Dropdown,
Icon,
Input,
Loader,
} from 'semantic-ui-react'
import { useMutation, useQuery } from 'react-apollo'
import gql from 'graphql-tag'
import styled from 'styled-components'
import { SectionTitle } from './SettingsPage'
import React from 'react'
import { useMutation } from 'react-apollo'
import { Button, Icon } from 'semantic-ui-react'
import PeriodicScanner from './PeriodicScanner'
import ScannerConcurrentWorkers from './ScannerConcurrentWorkers'
import { SectionTitle, InputLabelDescription } from './SettingsPage'
const SCAN_MUTATION = gql`
mutation scanAllMutation {
@@ -22,174 +15,15 @@ const SCAN_MUTATION = gql`
}
`
const SCAN_INTERVAL_QUERY = gql`
query scanIntervalQuery {
siteInfo {
periodicScanInterval
}
}
`
const SCAN_INTERVAL_MUTATION = gql`
mutation changeScanIntervalMutation($interval: Int!) {
setPeriodicScanInterval(interval: $interval)
}
`
const timeUnits = [
{
value: 'second',
multiplier: 1,
},
{
value: 'minute',
multiplier: 60,
},
{
value: 'hour',
multiplier: 60 * 60,
},
{
value: 'day',
multiplier: 60 * 60 * 24,
},
{
value: 'month',
multiplier: 60 * 60 * 24 * 30,
},
]
const convertToSeconds = ({ value, unit }) => {
return parseInt(value * timeUnits.find(x => x.value == unit).multiplier)
}
const convertToAppropriateUnit = ({ value, unit }) => {
if (value == 0) {
return {
unit: 'second',
value: 0,
}
}
const seconds = convertToSeconds({ value, unit })
let resultingUnit = timeUnits.first
for (const unit of timeUnits) {
if (
seconds / unit.multiplier >= 1 &&
(seconds / unit.multiplier) % 1 == 0
) {
resultingUnit = unit
} else {
break
}
}
return {
unit: resultingUnit.value,
value: seconds / resultingUnit.multiplier,
}
}
const InputLabelTitle = styled.p`
font-size: 1.1em;
font-weight: 600;
margin: 1em 0 0 !important;
`
const InputLabelDescription = styled.p`
font-size: 0.9em;
margin: 0 0 0.5em !important;
`
const ScannerSection = () => {
const [startScanner, { called }] = useMutation(SCAN_MUTATION)
const [enablePeriodicScanner, setEnablePeriodicScanner] = useState(false)
const [scanInterval, setScanInterval] = useState({
value: 4,
unit: 'minute',
})
const scanIntervalServerValue = useRef(null)
const scanIntervalQuery = useQuery(SCAN_INTERVAL_QUERY, {
onCompleted(data) {
const queryScanInterval = data.siteInfo.periodicScanInterval
if (queryScanInterval == 0) {
setScanInterval({
unit: 'second',
value: '',
})
} else {
setScanInterval(
convertToAppropriateUnit({
unit: 'second',
value: queryScanInterval,
})
)
}
setEnablePeriodicScanner(queryScanInterval > 0)
},
})
const [
setScanIntervalMutation,
{ loading: scanIntervalMutationLoading },
] = useMutation(SCAN_INTERVAL_MUTATION)
const onScanIntervalCheckboxChange = checked => {
setEnablePeriodicScanner(checked)
onScanIntervalUpdate(checked ? scanInterval : { value: 0, unit: 'second' })
}
const onScanIntervalUpdate = scanInterval => {
const seconds = convertToSeconds(scanInterval)
if (scanIntervalServerValue.current != seconds) {
setScanIntervalMutation({
variables: {
interval: convertToSeconds(scanInterval),
},
})
scanIntervalServerValue.current = seconds
}
}
const scanIntervalUnits = [
{
key: 'second',
text: 'Seconds',
value: 'second',
},
{
key: 'minute',
text: 'Minutes',
value: 'minute',
},
{
key: 'hour',
text: 'Hours',
value: 'hour',
},
{
key: 'day',
text: 'Days',
value: 'day',
},
{
key: 'month',
text: 'Months',
value: 'month',
},
]
return (
<div>
<SectionTitle nospace>Scanner</SectionTitle>
<InputLabelDescription>
Will scan all users for new or updated media
</InputLabelDescription>
<Button
icon
labelPosition="left"
@@ -199,68 +33,10 @@ const ScannerSection = () => {
disabled={called}
>
<Icon name="sync" />
Scan All
Scan all users
</Button>
<h3>Periodic scanner</h3>
<div style={{ margin: '12px 0' }}>
<Checkbox
label="Enable periodic scanner"
disabled={scanIntervalQuery.loading}
checked={enablePeriodicScanner}
onChange={(_, { checked }) => onScanIntervalCheckboxChange(checked)}
/>
</div>
{enablePeriodicScanner && (
<>
<label htmlFor="periodic_scan_field">
<InputLabelTitle>Periodic scan interval</InputLabelTitle>
<InputLabelDescription>
How often the scanner should perform automatic scans of all users
</InputLabelDescription>
</label>
<Input
label={
<Dropdown
onChange={(_, { value }) => {
const newScanInterval = {
...scanInterval,
unit: value,
}
setScanInterval(newScanInterval)
onScanIntervalUpdate(newScanInterval)
}}
value={scanInterval.unit}
options={scanIntervalUnits}
/>
}
onBlur={() => onScanIntervalUpdate(scanInterval)}
onKeyDown={({ key }) =>
key == 'Enter' && onScanIntervalUpdate(scanInterval)
}
loading={scanIntervalQuery.loading}
labelPosition="right"
style={{ maxWidth: 300 }}
id="periodic_scan_field"
value={scanInterval.value}
onChange={(_, { value }) => {
setScanInterval(x => ({
...x,
value,
}))
}}
/>
</>
)}
<Loader
active={scanIntervalQuery.loading || scanIntervalMutationLoading}
inline
size="small"
style={{ marginLeft: 16 }}
/>
<PeriodicScanner />
<ScannerConcurrentWorkers />
</div>
)
}

View File

@@ -12,6 +12,17 @@ export const SectionTitle = styled.h2`
border-bottom: 1px solid #ddd;
`
export const InputLabelTitle = styled.p`
font-size: 1.1em;
font-weight: 600;
margin: 1em 0 0 !important;
`
export const InputLabelDescription = styled.p`
font-size: 0.9em;
margin: 0 0 0.5em !important;
`
const SettingsPage = () => {
return (
<Layout>

View File

@@ -145,6 +145,7 @@ const UserRow = ({ user, refetchUsers }) => {
<Table.Row>
<Table.Cell>
<Input
style={{ width: '100%' }}
placeholder={user.username}
value={state.username}
onChange={e => updateInput(e, 'username')}
@@ -152,6 +153,7 @@ const UserRow = ({ user, refetchUsers }) => {
</Table.Cell>
<Table.Cell>
<Input
style={{ width: '100%' }}
placeholder={user.rootPath}
value={state.rootPath}
onChange={e => updateInput(e, 'rootPath')}