rendering admin config

This commit is contained in:
balzack 2025-02-17 22:31:28 -08:00
parent 10e5e75dfc
commit 680f0affb5
10 changed files with 256 additions and 34 deletions

View File

@ -39,4 +39,42 @@ export const styles = StyleSheet.create({
alignItems: 'center',
width: '100%',
},
busy: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: 32,
height: '100%',
},
option: {
width: '100%',
display: 'flex',
flexDirection: 'row',
alignItems: 'center',
paddingTop: 8,
paddingBottom: 8,
},
label: {
fontSize: 16,
},
inputSurface: {
flexGrow: 1,
marginRight: 8,
marginLeft: 16,
display: 'flex',
borderRadius: 8,
},
input: {
flexGrow: 1,
backgroundColor: 'transparent',
paddingTop: 0,
paddingBottom: 0,
display: 'flex',
height: 40,
maxHeight: 40,
borderRadius: 8,
},
inputUnderline: {
display: 'none',
},
});

View File

@ -1,23 +1,81 @@
import React from 'react';
import {SafeAreaView, Image, View, Pressable} from 'react-native';
import {Divider, Text} from 'react-native-paper';
import {ActivityIndicator, Surface, Divider, TextInput, Text} from 'react-native-paper';
import {styles} from './Setup.styled';
import {useSetup} from './useSetup.hook';
import {KeyboardAwareScrollView} from 'react-native-keyboard-aware-scroll-view';
import { Confirm } from '../confirm/Confirm';
export function Setup() {
const { state, actions } = useSetup();
const errorParams = {
title: state.strings.operationFailed,
prompt: state.strings.tryAgain,
cancel: {
label: state.strings.close,
action: actions.clearError,
},
};
return (
<View style={styles.setup}>
<View style={styles.header}>
<View style={styles.busy}>
{ state.loading && (
<ActivityIndicator size={18} />
)}
</View>
<Text style={styles.title}>{ state.strings.setup }</Text>
<View style={styles.busy}>
{ state.updating && (
<ActivityIndicator size={18} />
)}
</View>
</View>
<Divider style={styles.line} bold={true} />
<KeyboardAwareScrollView enableOnAndroid={true} style={styles.form} contentContainerStyle={styles.content}>
<Text>CONTENT</Text>
<View style={styles.option}>
<Text style={styles.label}>{ state.strings.federatedHost }</Text>
<Surface mode="flat" elevation={5} style={styles.inputSurface}>
<TextInput
dense={true}
style={styles.input}
outlineColor="transparent"
activeOutlineColor="transparent"
autoCapitalize={false}
underlineStyle={styles.inputUnderline}
mode="outlined"
disabled={state.loading}
placeholder={state.strings.hostHint}
value={state.setup?.domain}
onChangeText={value => actions.setDomain(value)}
/>
</Surface>
</View>
<View style={styles.option}>
<Text style={styles.label}>{ state.strings.storageLimit }</Text>
<Surface mode="flat" elevation={5} style={styles.inputSurface}>
<TextInput
type="number"
dense={true}
style={styles.input}
keyboardType="numeric"
outlineColor="transparent"
activeOutlineColor="transparent"
autoCapitalize={false}
underlineStyle={styles.inputUnderline}
mode="outlined"
disabled={state.loading}
placeholder={state.strings.storageHint}
value={state.accountStorage}
onChangeText={value => actions.setAccountStorage(value)}
/>
</Surface>
</View>
</KeyboardAwareScrollView>
<Divider style={styles.line} bold={true} />
<Confirm show={state.error} params={errorParams} />
</View>
);
}

View File

@ -2,14 +2,26 @@ import {useEffect, useState, useContext, useRef} from 'react';
import {AppContext} from '../context/AppContext';
import {DisplayContext} from '../context/DisplayContext';
import {ContextType} from '../context/ContextType';
import type { Member } from 'databag-client-sdk';
import type { Setup } from 'databag-client-sdk';
const DEBOUNCE_MS = 2000;
const DELAY_MS = 1000;
export function useSetup() {
const updated = useRef(false);
const loading = useRef(false);
const debounce = useRef(null);
const setup = useRef(null as null | Setup);
const app = useContext(AppContext);
const display = useContext(DisplayContext);
const [state, setState] = useState({
layout: '',
strings: {},
loading: true,
updating: false,
error: false,
accountStorage: '',
setup: null as null | Setup,
});
const updateState = (value: any) => {
@ -17,14 +29,41 @@ export function useSetup() {
};
const sync = async () => {
try {
const service = app.state.service;
//
} catch (err) {
console.log(err);
while (loading.current) {
try {
const service = app.state.service;
setup.current = await service.getSetup();
loading.current = false;
const storage = Math.floor(setup.current.accountStorage / 1073741824);
updateState({ setup: setup.current, accountStorage: storage.toString(), loading: false });
} catch (err) {
console.log(err);
await new Promise((r) => setTimeout(r, DELAY_MS));
}
}
}
const save = () => {
updated.current = true;
updateState({ updating: true });
clearTimeout(debounce.current);
debounce.current = setTimeout(async () => {
updated.current = false;
try {
const service = app.state.service;
await service.setSetup(setup.current);
if (updated.current) {
save()
} else {
updateState({ updating: false });
}
} catch (err) {
console.log(err);
updateState({ error: true });
}
}, DEBOUNCE_MS);
}
useEffect(() => {
const { layout, strings} = display.state;
updateState({ layout, strings});
@ -32,11 +71,33 @@ export function useSetup() {
useEffect(() => {
if (app.state.service) {
loading.current = true;
sync();
return () => {
loading.current = false;
}
}
}, [app.state.service]);
}, []);
const actions = {
clearError: () => {
updateState({ error: false });
},
setDomain: (domain: string) => {
if (setup.current) {
setup.current.domain = domain;
updateState({ setup: setup.current });
save();
}
},
setAccountStorage: (accountStorage: number) => {
if (setup.current) {
const storage = parseInt(accountStorage) * 1073741824;
setup.current.accountStorage = storage;
updateState({ setup: setup.current, accountStorage });
save();
}
},
};
return {state, actions};

View File

@ -225,7 +225,7 @@ export type ProfileEntity = {
};
export type AccountEntity = {
acconutId: number;
accountId: number;
guid: string;
handle: string;
name: string;
@ -251,6 +251,26 @@ export const defaultProfileEntity = {
node: '',
};
export type SetupEntity = {
domain: string;
accountStorage: number;
enableImage: boolean;
enableAudio: boolean;
enableVideo: boolean;
enableBinary: boolean;
keyType: string;
pushSupported: boolean;
allowUnsealed: boolean;
transformSupported: boolean;
enableIce: boolean;
iceService: string;
iceUrl: string;
iceUsername: string;
icePassword: string;
enableOpenAccess: boolean;
openAccessLimit: number;
};
export type Calling = {
id: string;
cardId: string;

View File

@ -0,0 +1,4 @@
export function getMemberImageUrl(server: string, secure: boolean, token: string, accountId: number, revision: number) {
return `http${secure ? 's' : ''}://${server}/admin/accounts/${accountId}/image?token=${token}&revision=${revision}`;
}

View File

@ -0,0 +1,9 @@
import { checkResponse, fetchWithTimeout } from './fetchUtil';
import { AccountEntity } from '../entities';
export async function getMembers(server: string, secure: boolean, token: string): Promise<AccountEntity[]> {
const endpoint = `http${secure ? 's' : ''}://${server}/admin/accounts?token=${token}`;
const accounts = await fetchWithTimeout(endpoint, { method: 'GET' });
checkResponse(accounts.status);
return await accounts.json();
}

View File

@ -0,0 +1,10 @@
import { checkResponse, fetchWithTimeout } from './fetchUtil';
import { SetupEntity } from '../entities';
export async function getNodeConfig(server: string, secure: boolean, token: string): Promise<ConfigEntity> {
const endpoint = `http${secure ? 's' : ''}://${server}/admin/config?token=${token}`;
const config = await fetchWithTimeout(endpoint, { method: 'GET' });
checkResponse(config.status);
return await config.json();
}

View File

@ -0,0 +1,9 @@
import { checkResponse, fetchWithTimeout } from './fetchUtil';
import { SetupEntity } from '../entities';
export async function setNodeConfig(server: string, secure: boolean, token: string, config: SetupEntity) {
const endpoint = `http${secure ? 's' : ''}://${server}/admin/config?token=${token}`;
const { status }= await fetchWithTimeout(endpoint, { method: 'PUT', body: JSON.stringify(config) });
checkResponse(status);
}

View File

@ -8,6 +8,8 @@ import { getAdminMFAuth } from './net/getAdminMFAuth';
import { setAdminMFAuth } from './net/setAdminMFAuth';
import { addAdminMFAuth } from './net/addAdminMFAuth';
import { removeAdminMFAuth } from './net/removeAdminMFAuth';
import { getNodeConfig } from './net/getNodeConfig';
import { setNodeConfig } from './net/setNodeConfig';
export class ServiceModule implements Service {
private log: Logging;
@ -45,28 +47,29 @@ export class ServiceModule implements Service {
}
public async getSetup(): Promise<Setup> {
return {
domain: '',
accountStorage: '',
enableImage: true,
enableAudio: true,
enableVideo: true,
enableBinary: true,
keyType: '',
pushSupported: true,
allowUnsealed: true,
transformSupported: true,
enableIce: true,
iceService: '',
iceUrl: '',
iceUsername: '',
icePassword: '',
enableOpenAccess: true,
openAccessLimit: 0,
};
}
const { node, secure, token } = this;
const entity = await getNodeConfig(node, secure, token);
const { domain, accountStorage, enableImage, enableAudio, enableVideo, enableBinary,
keyType, pushSupported, allowUnsealed, transformSupported, enableIce, iceService,
iceUrl, iceUsername, icePassword, enableOpenAccess, openAccessLimit } = entity;
const service = iceService ? iceService : 'default';
const setup = { domain, accountStorage, enableImage, enableAudio, enableVideo, enableBinary,
keyType, pushSupported, allowUnsealed, transformSupported, enableIce, iceService: service,
iceUrl, iceUsername, icePassword, enableOpenAccess, openAccessLimit };
return setup;
}
public async setSetup(config: Setup): Promise<void> {}
public async setSetup(setup: Setup): Promise<void> {
const { node, secure, token } = this;
const { domain, accountStorage, enableImage, enableAudio, enableVideo, enableBinary,
keyType, pushSupported, allowUnsealed, transformSupported, enableIce, iceService,
iceUrl, iceUsername, icePassword, enableOpenAccess, openAccessLimit } = setup;
const service = iceService === 'default' ? null : iceService;
const entity = { domain, accountStorage, enableImage, enableAudio, enableVideo, enableBinary,
keyType, pushSupported, allowUnsealed, transformSupported, enableIce, iceService: service,
iceUrl, iceUsername, icePassword, enableOpenAccess, openAccessLimit };
await setNodeConfig(node, secure, token, entity);
}
public async enableMFAuth(): Promise<{ image: string, text: string}> {
const { node, secure, token } = this;

View File

@ -222,19 +222,29 @@ export type Member = {
storageUsed: number,
};
export enum KeyType {
RSA_4096 = 'RSA4096',
RSA_2048 = 'RSA2048',
}
export enum ICEService {
Cloudflare = 'cloudflage',
Default = 'default',
}
export type Setup = {
domain: string;
accountStorage: string;
accountStorage: number;
enableImage: boolean;
enableAudio: boolean;
enableVideo: boolean;
enableBinary: boolean;
keyType: string;
keyType: KeyType;
pushSupported: boolean;
allowUnsealed: boolean;
transformSupported: boolean;
enableIce: boolean;
iceService: string;
iceService: ICEService;
iceUrl: string;
iceUsername: string;
icePassword: string;