Flag manipulation
This commit is contained in:
parent
149166eb1c
commit
d775b65738
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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>;
|
||||
};
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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;
|
Loading…
Reference in New Issue
Block a user