Flag manipulation

This commit is contained in:
Erik 2023-07-17 18:52:55 +03:00
parent 149166eb1c
commit d775b65738
Signed by: Navy.gif
GPG Key ID: 2532FBBB61C65A68
6 changed files with 160 additions and 81 deletions

View File

@ -54,9 +54,9 @@ export type Role = {
rateLimits: RateLimits
} & APIEntity
type FlagType = string | number | boolean | number[] | null
type FlagEnv = 'test' | 'prod'
type FlagConsumer = 'client' | 'server' | 'api'
export type FlagType = string | number | boolean | number[] | null
// type FlagEnv = 'test' | 'prod'
// type FlagConsumer = 'client' | 'server' | 'api'
export type Flag = {
id: string,

View File

@ -50,25 +50,27 @@ export type InputElementProperties<T> = {
inputRef?: React.RefObject<HTMLInputElement>,
onChange?: React.ChangeEventHandler<HTMLInputElement>,
children?: React.ReactNode, //JSX.Element | JSX.Element[] | string,
placeholder?: string
placeholder?: string,
required?: boolean,
name?: string
}
export const ToggleSwitch = ({ value, onChange, inputRef, children }: InputElementProperties<boolean>) =>
export const ToggleSwitch = ({ name, value, onChange, inputRef, children }: InputElementProperties<boolean>) =>
{
return <label className="label-fix">
<span className="check-box check-box-row mb-2">
{children && <b>{children}</b>}
<input ref={inputRef} defaultChecked={value} onChange={onChange} type='checkbox' />
<input name={name} ref={inputRef} defaultChecked={value} onChange={onChange} type='checkbox' />
</span>
</label>;
};
export const VerticalToggleSwitch = ({ value, onChange, inputRef, children }: InputElementProperties<boolean>) =>
export const VerticalToggleSwitch = ({ name, value, onChange, inputRef, children }: InputElementProperties<boolean>) =>
{
return <label className="label-fix">
{children && <b>{children}</b>}
<span className="check-box">
<input ref={inputRef} defaultChecked={value} onChange={onChange} type='checkbox' />
<input name={name} ref={inputRef} defaultChecked={value} onChange={onChange} type='checkbox' />
</span>
</label>;
};
@ -82,7 +84,7 @@ type StringInputProperties = {
maxLength?: number,
minLength?: number
} & ManualInputProperties<string>
export const StringInput = ({ value, onChange, inputRef, children, placeholder, onBlur, onKeyUp, minLength, maxLength }: StringInputProperties) =>
export const StringInput = ({ name, value, onChange, inputRef, children, placeholder, onBlur, onKeyUp, minLength, maxLength, required = false }: StringInputProperties) =>
{
const input = <input
@ -94,6 +96,8 @@ export const StringInput = ({ value, onChange, inputRef, children, placeholder,
onKeyUp={onKeyUp}
maxLength={maxLength}
minLength={minLength}
required={required}
name={name}
/>;
if (children)
return <label>
@ -110,17 +114,10 @@ export type NumberInputProperties = {
step?: number
} & ManualInputProperties<number>
export const NumberInput = ({ children, placeholder, inputRef, onChange, value, min, max, type, step }: NumberInputProperties) =>
export const NumberInput = ({ name, children, placeholder, inputRef, onChange, value, min, max, type, step, required = false }: NumberInputProperties) =>
{
if (typeof step === 'undefined')
{
if (type === 'float')
step = 0.1;
else if (type === 'int')
step = 1;
else
step = 1;
}
if (typeof step === 'undefined' && type === 'int')
step = 1;
const input = <input
placeholder={placeholder}
@ -131,6 +128,8 @@ export const NumberInput = ({ children, placeholder, inputRef, onChange, value,
min={min}
max={max}
step={step}
required={required}
name={name}
/>;
if (children)

View File

@ -1,11 +1,11 @@
import React from 'react';
import '../css/components/PageElements.css';
export const Popup = ({ display = false, children }: { display: boolean, children: React.ReactElement }) =>
export const Popup = ({ display = false, children, className = '' }: { className?: string, display: boolean, children: React.ReactElement }) =>
{
if (!display)
return null;
return <div className='popup'>
return <div className={`popup ${className}`}>
{children}
</div>;
};

View File

@ -759,3 +759,14 @@ input[type=file]::file-selector-button:hover {
display: block;
width: fit-content;
}
/* IDK if this is a good way of doing this */
input:is([type=text]):required {
border: 2px solid yellow !important;
}
input:is([type=text]):invalid {
border: 2px solid red !important;
}
input:is([type=text]):valid {
border: 2px solid green !important;
}

View File

@ -35,6 +35,13 @@ details .flag {
.flag span.clickable:hover{
font-style: italic;
}
.create-flag {
width: 35%;
max-width: 400px;
left: 44vw;
}
.list.category {
user-select: none;
background-color: var(--color-darkGrey);

View File

@ -1,12 +1,12 @@
import React, { useEffect, useRef, useState } from 'react';
import { Flag as APIFlag, FlagCreateData } from '../../@types/ApiStructures';
import { OrganisedFlags, capitalise, get, getSetting, organiseFlags, patch, setSetting } from '../../util/Util';
import React, { ReactElement, useContext, useEffect, useRef, useState } from 'react';
import { Flag as APIFlag, FlagCreateData, FlagType } from '../../@types/ApiStructures';
import { OrganisedFlags, capitalise, del, get, getSetting, organiseFlags, patch, post, setSetting } from '../../util/Util';
import { ClickToEdit, Dropdown, NumberInput, StringInput, ToggleSwitch } from '../../components/InputElements';
import { PageButtons } from '../../components/PageControls';
import { Popup } from '../../components/PageElements';
import ClickDetector from '../../util/ClickDetector';
const emptyFlag = {
const emptyFlag: FlagCreateData = {
name: '',
env: '',
consumer: '',
@ -14,13 +14,30 @@ const emptyFlag = {
hierarchy: ''
};
const Context = React.createContext<{ flags: APIFlag[], updateFlags: (flags: APIFlag[]) => void }>({ flags: [], updateFlags: () => null });
const FlagContext = ({ children }: { children: React.ReactNode }) =>
{
const [ flags, updateFlags ] = useState<APIFlag[]>([]);
return <Context.Provider value={{ flags, updateFlags }}>
{children}
</Context.Provider>;
};
const Flag = ({ flag: incoming, onChange }: { flag: APIFlag, onChange?: (flag: APIFlag) => void }) =>
{
const { flags, updateFlags } = useContext(Context);
const [ flag, setFlag ] = useState(incoming);
const [ error, setError ] = useState<string>();
const valueRef = useRef<HTMLInputElement>(null);
const deleteFlag = async () =>
{
const response = await del(`/api/flags/${flag.id}`);
if (!response.success)
return setError(response.message);
updateFlags(flags.filter(f => f.id !== flag.id));
};
const updateFlag = (f: APIFlag) =>
{
f.edited = true;
@ -31,7 +48,6 @@ const Flag = ({ flag: incoming, onChange }: { flag: APIFlag, onChange?: (flag: A
const save = async () =>
{
console.log(flag);
const response = await patch(`/api/flags/${flag.id}`, flag);
if (response.success)
{
@ -44,7 +60,7 @@ const Flag = ({ flag: incoming, onChange }: { flag: APIFlag, onChange?: (flag: A
}
};
let Input = <p>Loading...</p>;
let Input: ReactElement | null = null;
if (flag.type === 'string')
Input = <StringInput onChange={({ target }) => updateFlag({ ...flag, value: target.value })} inputRef={valueRef} value={flag.value as string} />;
else if (flag.type === 'number')
@ -72,9 +88,9 @@ const Flag = ({ flag: incoming, onChange }: { flag: APIFlag, onChange?: (flag: A
</p>
<p className='mt-0 mb-1'>
<b>Value: </b>
{Input}
{Input ?? 'Loading...'}
</p>
<button className='button danger'>Delete</button>
<button onClick={deleteFlag} className='button danger'>Delete</button>
{flag.edited && <button onClick={save} className='button primary'>Save</button>}
</div>;
@ -134,7 +150,6 @@ const ListCategory = ({ flags, name, onChange }: { flags: OrganisedFlags, name:
const ListView = ({ flags }: { flags: APIFlag[] }) =>
{
const [ organised, updateFlags ] = useState<OrganisedFlags>({});
useEffect(() =>
{
updateFlags(organiseFlags(flags));
@ -178,11 +193,93 @@ const TileView = ({ flags, children }: { flags: APIFlag[], children: React.React
</div>;
};
const CreateFlag = ({ close, submit }: { close: () => void, submit: (data: FlagCreateData) => Promise<{error?: string}> }) =>
{
const [ flag, updateFlag ] = useState<FlagCreateData>(emptyFlag);
const [ flagType, setType ] = useState<string>('');
const [ error, setError ] = useState('');
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) =>
{
const { target } = e;
const { value } = target;
updateFlag({ ...flag, [target.name]: value });
};
const changeFlagType = (e: React.ChangeEvent<HTMLSelectElement>) =>
{
const { target } = e;
setType(target.value);
};
const createFlag = async (e: React.FormEvent<HTMLFormElement>) =>
{
e.preventDefault();
let value: FlagType = flag.value as string;
if (flagType === 'int')
value = parseInt(value);
else if (flagType === 'float')
value = parseFloat(value);
else if(flagType ==='boolean')
value = Boolean(value);
const result = await submit({ ...flag, value });
if (result.error)
return setError(result.error);
updateFlag({ ...emptyFlag });
close();
};
const cancel = (e: React.FormEvent) =>
{
e.preventDefault();
updateFlag({ ...emptyFlag });
close();
};
const props = { name: 'value', onChange: handleChange, required: true, placeholder: 'Value' };
let valueInput = null;
if (flagType === 'string')
valueInput = <StringInput {...props} />;
else if (flagType === 'float')
valueInput = <NumberInput {...props} type='float' />;
else if (flagType === 'int')
valueInput = <NumberInput {...props} type='int' />;
else if (flagType === 'boolean')
valueInput = <ToggleSwitch {...props} />;
return <div className='card'>
<h4>Create Flag</h4>
{error}
<form onSubmit={createFlag} onReset={cancel}>
<label>Name</label>
<input onChange={handleChange} minLength={4} name='name' autoComplete='off' placeholder='Name' type='text' required />
<label>Environment</label>
<input onChange={handleChange} name='env' autoComplete='off' placeholder='Environment' type='text' required />
<label>Consumer</label>
<input onChange={handleChange} name='consumer' autoComplete='off' placeholder='Consumer' type='text' required />
<label>Hierarchy</label>
<input onChange={handleChange} pattern='(\w+(?::\w+)*)' name='hierarchy' autoComplete='off' placeholder='Hierarchy' type='text' />
<label>Flag type</label>
<select onChange={changeFlagType}>
<option value=''>Select one</option>
<option value='float'>Float</option>
<option value='int'>Integer</option>
<option value='string'>String</option>
<option value='boolean'>Boolean</option>
</select>
{flagType && <label>Value</label>}
{valueInput}
<button type='reset' className='button danger'>Cancel</button>
{flagType && <button type='submit' className='button primary'>Create</button>}
</form>
</div>;
};
const Flags = () =>
{
const { flags, updateFlags: setFlags } = useContext(Context);
const searchBarRef = useRef<HTMLInputElement>(null);
const [ flags, setFlags ] = useState<APIFlag[]>([]);
const [ error, setError ] = useState<string | null>(null);
const [ selectedFilters, setSelectedFilters ] = useState<string[]>([]);
const [ availableFilters, setAvailableFilters ] = useState<string[] | null>(null);
@ -198,9 +295,13 @@ const Flags = () =>
await setSetting('user:flags:listView', val);
};
const createFlag = (data: FlagCreateData) =>
const createFlag = async (data: FlagCreateData): Promise<{error?: string}> =>
{
console.log('submit flag', data);
const response = await post('/api/flags', data);
if (!response.success)
return { error: response.message };
setFlags([ ...flags, response.data as APIFlag ]);
return { };
};
useEffect(() =>
@ -224,7 +325,6 @@ const Flags = () =>
query.all = true;
const response = await get('/api/flags', query);
if (!response.success || !response.data)
{
setFlags([]);
@ -240,11 +340,10 @@ const Flags = () =>
})();
}, [ selectedFilters, nameSearch, page, listView ]);
return <div className='row'>
<ClickDetector callback={() => togglePopup(false)}>
<Popup display={displayPopup}>
<Popup display={displayPopup} className='col-12-lg col-12 create-flag'>
<CreateFlag submit={createFlag} close={() => togglePopup(false)} />
</Popup>
</ClickDetector>
@ -318,48 +417,11 @@ const Flags = () =>
};
const CreateFlag = ({ close, submit }: { close: () => void, submit: (data: FlagCreateData) => void }) =>
const Wrapper = () =>
{
const [ flag, updateFlag ] = useState<FlagCreateData>(emptyFlag);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) =>
{
const { target } = e;
updateFlag({ ...flag, [target.name]: target.value });
};
const createFlag = (e: React.FormEvent) =>
{
e.preventDefault();
submit({ ...flag });
updateFlag({ ...emptyFlag });
};
const cancel = (e: React.FormEvent) =>
{
e.preventDefault();
updateFlag({ ...emptyFlag });
close();
};
return <div className='col-6-lg col-12'>
<div className='card'>
<h4>Create Flag</h4>
<form>
<label>Name</label>
<input onChange={handleChange} name='name' autoComplete='off' placeholder='Name' type='text' required />
<label>Environment</label>
<input onChange={handleChange} name='env' autoComplete='off' placeholder='Environment' type='text' required />
<label>Consumer</label>
<input onChange={handleChange} name='consumer' autoComplete='off' placeholder='Consumer' type='text' required />
<label>Hierarchy</label>
<input onChange={handleChange} name='hierarchy' autoComplete='off' placeholder='Hierarchy' type='text' />
<label>Value</label>
<input onChange={handleChange} name='value' autoComplete='off' placeholder='Name' type='text' required />
<button className='button danger' onClick={cancel}>Cancel</button>
<button className='button primary' onClick={createFlag}>Create</button>
</form>
</div>
</div>;
return <FlagContext>
<Flags />
</FlagContext>;
};
// const Flags = () =>
@ -373,4 +435,4 @@ const CreateFlag = ({ close, submit }: { close: () => void, submit: (data: FlagC
// };
export default Flags;
export default Wrapper;