Flags page
Dropdown menu + other input elements improved pagination elements
This commit is contained in:
parent
6476a205a4
commit
d6d7de4d93
@ -63,5 +63,6 @@ export type Flag = {
|
||||
name: string,
|
||||
env: FlagEnv,
|
||||
consumer: FlagConsumer,
|
||||
value: FlagType
|
||||
value: FlagType,
|
||||
type: string
|
||||
}
|
@ -38,36 +38,91 @@ export const FileSelector = ({ cb }: { cb: (file: File) => void }) => {
|
||||
</label>;
|
||||
};
|
||||
|
||||
export const ToggleSwitch = ({ value, onChange, ref, children }:
|
||||
{ value: boolean, ref?: React.RefObject<HTMLInputElement>, onChange?: React.ChangeEventHandler, children: string }) => {
|
||||
export type InputElementProperties<T> = {
|
||||
value?: T,
|
||||
inputRef?: React.RefObject<HTMLInputElement>,
|
||||
onChange?: React.ChangeEventHandler,
|
||||
children?: string,
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
export const ToggleSwitch = ({ value, onChange, inputRef, children }: InputElementProperties<boolean>) => {
|
||||
return <p>
|
||||
<label>
|
||||
<span className="check-box check-box-row">
|
||||
<b>{children}</b>
|
||||
<input ref={ref} defaultChecked={value} onChange={onChange} type='checkbox' />
|
||||
{children && <b>{children}</b>}
|
||||
<input ref={inputRef} defaultChecked={value} onChange={onChange} type='checkbox' />
|
||||
</span>
|
||||
</label>
|
||||
</p>;
|
||||
};
|
||||
|
||||
export const VerticalToggleSwitch = ({ value, onChange, ref, children }:
|
||||
{ value: boolean, ref?: React.RefObject<HTMLInputElement>, onChange?: React.ChangeEventHandler, children: string }) => {
|
||||
export const VerticalToggleSwitch = ({ value, onChange, inputRef, children }: InputElementProperties<boolean>) => {
|
||||
return <p>
|
||||
<label>
|
||||
<b>{children}</b>
|
||||
{children && <b>{children}</b>}
|
||||
<span className="check-box">
|
||||
<input ref={ref} defaultChecked={value} onChange={onChange} type='checkbox' />
|
||||
<input ref={inputRef} defaultChecked={value} onChange={onChange} type='checkbox' />
|
||||
</span>
|
||||
</label>
|
||||
</p>;
|
||||
};
|
||||
|
||||
export const NumberInput = () => {
|
||||
//
|
||||
type ManualInputProperties<T> = {
|
||||
onBlur?: React.FocusEventHandler,
|
||||
onKeyUp?: React.KeyboardEventHandler
|
||||
} & InputElementProperties<T>;
|
||||
|
||||
type StringInputProperties = {
|
||||
maxLength?: number,
|
||||
minLength?: number
|
||||
} & ManualInputProperties<string>
|
||||
export const StringInput = ({ value, onChange, inputRef, children, placeholder, onBlur, onKeyUp, minLength, maxLength }: StringInputProperties) => {
|
||||
return <label>
|
||||
{children && <b>{children}</b>}
|
||||
<input
|
||||
onBlur={onBlur}
|
||||
value={value}
|
||||
placeholder={placeholder}
|
||||
ref={inputRef}
|
||||
onChange={onChange}
|
||||
onKeyUp={onKeyUp}
|
||||
maxLength={maxLength}
|
||||
minLength={minLength}
|
||||
/>
|
||||
</label>;
|
||||
};
|
||||
|
||||
export type NumberInputProperties = {
|
||||
min?: number,
|
||||
max?: number,
|
||||
type?: 'float' | 'int',
|
||||
step?: number
|
||||
} & ManualInputProperties<number>
|
||||
|
||||
export const NumberInput = ({ children, placeholder, inputRef, onChange, value, min, max, type, step }: NumberInputProperties) => {
|
||||
if (typeof step === 'undefined') {
|
||||
if (type === 'float') step = 0.1;
|
||||
else if (type === 'int') step = 1;
|
||||
else step = 1;
|
||||
}
|
||||
return <label>
|
||||
{children && <b>{children}</b>}
|
||||
<input
|
||||
placeholder={placeholder}
|
||||
ref={inputRef}
|
||||
defaultValue={value}
|
||||
onChange={onChange}
|
||||
type="number"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
/>
|
||||
</label>;
|
||||
};
|
||||
|
||||
const DropdownHeader = ({ children, className = '' }: DropdownBaseProps) => {
|
||||
return <summary className={`card is-vertical-align dropdown-header p-2 ${className}`}>
|
||||
return <summary className={`card is-vertical-align header p-2 ${className}`}>
|
||||
{children}
|
||||
</summary>;
|
||||
};
|
||||
@ -78,16 +133,19 @@ const DropdownItem = ({ children, type, selected, onClick }: DropdownItemProps)
|
||||
InnerElement = <ToggleSwitch value={selected || false} onChange={onClick}>
|
||||
{children as string}
|
||||
</ToggleSwitch>;
|
||||
else InnerElement = <div onClick={onClick}>
|
||||
else InnerElement = <div onClick={(event) => {
|
||||
event.preventDefault();
|
||||
if(onClick) onClick(event);
|
||||
}}>
|
||||
{children}
|
||||
</div>;
|
||||
return <div className='dropdown-item'>
|
||||
return <div className='card item'>
|
||||
{InnerElement}
|
||||
</div>;
|
||||
};
|
||||
|
||||
const DropdownItemList = ({ children }: DropdownBaseProps) => {
|
||||
return <div className='card w-100 '>
|
||||
const DropdownItemList = ({ children, className = '' }: DropdownBaseProps) => {
|
||||
return <div className={`card item-list ${className}`}>
|
||||
{children}
|
||||
</div>;
|
||||
};
|
||||
@ -126,4 +184,9 @@ const Dropdown = Object.assign(DropdownComp, {
|
||||
Item: DropdownItem
|
||||
});
|
||||
|
||||
export type InputElementType =
|
||||
| ((props: InputElementProperties<string>) => React.ReactElement)
|
||||
| ((props: InputElementProperties<boolean>) => React.ReactElement)
|
||||
| ((props: NumberInputProperties) => React.ReactElement);
|
||||
|
||||
export { Dropdown };
|
@ -1,14 +1,23 @@
|
||||
import React from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
export const PageButtons = ({ setPage, page, length }: { setPage: (page: number) => void, page: number, length: number}) => {
|
||||
type PageButtonProps = {
|
||||
setPage: (page: number) => void,
|
||||
page: number,
|
||||
pages: number
|
||||
}
|
||||
|
||||
export const PageButtons = ({ setPage, page, pages }: PageButtonProps) => {
|
||||
return <div className='flex is-vertical-align is-center page-controls'>
|
||||
<button className="button dark" onClick={() => setPage(page - 1 || 1)}>Previous</button>
|
||||
<p>Page: {page}</p>
|
||||
<button className="button dark" onClick={() => {
|
||||
if (length === 10) setPage(page + 1);
|
||||
<button className="button dark" disabled={page === 1} onClick={() => {
|
||||
setPage(page - 1 || 1);
|
||||
}}>Previous</button>
|
||||
<p>Page: {page} / {pages}</p>
|
||||
<button className="button dark" disabled={page === pages} onClick={() => {
|
||||
if (page < pages)
|
||||
setPage(page + 1);
|
||||
}}>Next</button>
|
||||
</div>;
|
||||
</div>;
|
||||
};
|
||||
|
||||
export const BackButton = () => {
|
||||
|
@ -1,3 +1,19 @@
|
||||
.role-selector {
|
||||
.dropdown .header {
|
||||
min-height: 40px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.dropdown .item-list {
|
||||
background-color: var(--bg-color);
|
||||
width: calc(100% - 40px);
|
||||
min-height: 26px;
|
||||
max-height: 200px;
|
||||
overflow: auto;
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.dropdown .item {
|
||||
margin: 2px;
|
||||
padding: 0.5rem 1rem !important;
|
||||
user-select: none;
|
||||
}
|
@ -82,6 +82,11 @@ table.striped tr:nth-of-type(2n) {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.button.danger {
|
||||
background-color: var(--color-error);
|
||||
color: var(--font-color);
|
||||
}
|
||||
|
||||
.page-controls button{
|
||||
min-width: 120px;
|
||||
}
|
||||
|
@ -0,0 +1,11 @@
|
||||
.flag-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px
|
||||
}
|
||||
|
||||
.flag {
|
||||
background-color: var(--bg-color);
|
||||
/* flex: 1 0 21%; */
|
||||
width: calc(25% - 8px);
|
||||
}
|
@ -95,5 +95,10 @@ li input {
|
||||
}
|
||||
|
||||
.role {
|
||||
margin: 5px
|
||||
margin: 2px;
|
||||
padding: 0.5rem 1rem !important;
|
||||
}
|
||||
|
||||
.role-selector {
|
||||
background-color: white;
|
||||
}
|
@ -1,33 +1,119 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Flag as APIFlag } from "../../@types/ApiStructures";
|
||||
import { get } from "../../util/Util";
|
||||
import { ToggleSwitch } from "../../components/InputElements";
|
||||
import { Dropdown, InputElementType, NumberInput, StringInput, ToggleSwitch } from "../../components/InputElements";
|
||||
import { PageButtons } from "../../components/PageControls";
|
||||
|
||||
const inputTypes = {
|
||||
boolean: ToggleSwitch
|
||||
|
||||
};
|
||||
const ValidFilters = ['env:prod', 'env:test', 'consumer:client', 'consumer:server', 'consumer:api'];
|
||||
|
||||
const Flag = ({ flag }: { flag: APIFlag }) => {
|
||||
|
||||
return <div className='flag card'>
|
||||
<p><b>Name:</b> {flag.name}</p>
|
||||
<p><b>ID:</b> {flag.id}</p>
|
||||
<p><b>Environment:</b> {flag.env}</p>
|
||||
<p><b>Consumer:</b> {flag.consumer}</p>
|
||||
<label><b>Value:</b> <input /></label>
|
||||
let Input = <p>Loading...</p>;
|
||||
if (flag.type === 'string') Input = <StringInput value={flag.value as string}>
|
||||
Value:
|
||||
</StringInput>;
|
||||
else if (flag.type === 'number') Input = <NumberInput value={flag.value as number} type='float'>
|
||||
Value:
|
||||
</NumberInput>;
|
||||
else if (flag.type === 'boolean') Input = <ToggleSwitch value={flag.value as boolean}>
|
||||
Value:
|
||||
</ToggleSwitch>;
|
||||
|
||||
return <div className='flag card mt-0 mb-0'>
|
||||
{/* <p className="mt-0 mb-1"><b>Name:</b> {flag.name}</p> */}
|
||||
<h3 className="mt-0 mb-1">{flag.name}</h3>
|
||||
<button className="button danger">Delete</button>
|
||||
<button className="button primary">Save</button>
|
||||
<p className="mt-0 mb-1"><b>ID:</b> {flag.id}</p>
|
||||
<p className="mt-0 mb-1"><b>Environment:</b> {flag.env}</p>
|
||||
<p className="mt-0 mb-1"><b>Consumer:</b> {flag.consumer}</p>
|
||||
{Input}
|
||||
</div>;
|
||||
|
||||
};
|
||||
|
||||
const FlagList = ({ flags, error }: { flags: APIFlag[], error: string | null }) => {
|
||||
|
||||
console.log(flags);
|
||||
return <div className='col-6-lg col-12'>
|
||||
const FlagList = () => {
|
||||
|
||||
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(ValidFilters);
|
||||
const [nameSearch, setNameSearch] = useState<string>('');
|
||||
const [page, setPage] = useState(1);
|
||||
const [pages, setPages] = useState(1);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const query: { [key: string]: string[] } = {};
|
||||
for (const selected of selectedFilters) {
|
||||
const [prop, val] = selected.split(':');
|
||||
if (!query[prop]) query[prop] = [val];
|
||||
else query[prop].push(val);
|
||||
}
|
||||
if (nameSearch) query.name = [nameSearch];
|
||||
const response = await get('/api/flags', query);
|
||||
console.log(response);
|
||||
if (!response.success || !response.data) {
|
||||
setFlags([]);
|
||||
return setError(response.message as string);
|
||||
}
|
||||
setFlags(response.data.flags as APIFlag[]);
|
||||
setPages(response.data.pages);
|
||||
setError(null);
|
||||
})();
|
||||
}, [selectedFilters, nameSearch, page]);
|
||||
|
||||
return <div className='col-12-lg col-12'>
|
||||
|
||||
<h4>Search</h4>
|
||||
<div>
|
||||
<StringInput
|
||||
inputRef={searchBarRef}
|
||||
onBlur={() => {
|
||||
setNameSearch(searchBarRef.current?.value || '');
|
||||
}}
|
||||
onKeyUp={(event) => {
|
||||
if (event.key === 'Enter')
|
||||
setNameSearch(searchBarRef.current?.value || '');
|
||||
}}
|
||||
placeholder="Flag name or ID"
|
||||
/>
|
||||
<Dropdown>
|
||||
<Dropdown.Header>
|
||||
{selectedFilters.map(f => <Dropdown.Item
|
||||
key={f}
|
||||
onClick={() => {
|
||||
setSelectedFilters(selectedFilters.filter(ff => f !== ff));
|
||||
setAvailableFilters([...availableFilters, f]);
|
||||
}}
|
||||
>
|
||||
{f}
|
||||
</Dropdown.Item>)}
|
||||
</Dropdown.Header>
|
||||
|
||||
<Dropdown.ItemList>
|
||||
{availableFilters.map(f => <Dropdown.Item
|
||||
key={f}
|
||||
onClick={() => {
|
||||
setAvailableFilters(availableFilters.filter(ff => f !== ff));
|
||||
setSelectedFilters([...selectedFilters, f]);
|
||||
}}
|
||||
>
|
||||
{f}
|
||||
</Dropdown.Item>)}
|
||||
</Dropdown.ItemList>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<h4>Flags</h4>
|
||||
{error && <p>{error}</p>}
|
||||
|
||||
<div className="flag-list">
|
||||
{flags.map(flag => <Flag key={flag.id} flag={flag} />)}
|
||||
</div>
|
||||
|
||||
<PageButtons {...{ page, setPage, pages }} />
|
||||
|
||||
</div>;
|
||||
|
||||
@ -45,20 +131,9 @@ const CreateFlag = () => {
|
||||
|
||||
const Flags = () => {
|
||||
|
||||
const [flags, setFlags] = useState<APIFlag[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const response = await get('/api/flags');
|
||||
if (!response.success) return setError(response.message as string);
|
||||
setFlags(response.data as APIFlag[]);
|
||||
})();
|
||||
}, []);
|
||||
|
||||
return <div className='row'>
|
||||
|
||||
<FlagList flags={flags} error={error} />
|
||||
<FlagList />
|
||||
|
||||
<CreateFlag />
|
||||
|
||||
|
@ -99,14 +99,16 @@ const Roles = () => {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [roles, setRoles] = useState<R[]>([]);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pages, setPages] = useState(1);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const result = await get('/api/roles', { page });
|
||||
if (result.success) {
|
||||
if (result.success && result.data) {
|
||||
setError(null);
|
||||
setRoles(result.data as R[]);
|
||||
setRoles(result.data.roles as R[]);
|
||||
setPages(result.data.pages);
|
||||
} else {
|
||||
setError(result.message || 'Unknown error');
|
||||
}
|
||||
@ -137,7 +139,7 @@ const Roles = () => {
|
||||
/>)}
|
||||
</Table>
|
||||
|
||||
<PageButtons {...{page, setPage, length: roles.length}} />
|
||||
<PageButtons {...{page, setPage, pages}} />
|
||||
|
||||
</div>
|
||||
|
||||
|
@ -78,7 +78,7 @@ const Application = ({ app }: { app: App }) => {
|
||||
};
|
||||
|
||||
const RoleComp = ({ role, onClick }: { role: Role, onClick: React.ReactEventHandler }) => {
|
||||
return <div className='role card p-3' >
|
||||
return <div className='card role' >
|
||||
{role.name} <span className="clickable" onClick={onClick}>X</span>
|
||||
</div>;
|
||||
};
|
||||
@ -99,7 +99,8 @@ const Roles = ({ userRoles, roles }: { userRoles: Role[], roles: Role[] }) => {
|
||||
return <Dropdown>
|
||||
|
||||
<Dropdown.Header className='role-selector'>
|
||||
{equippedRoles.map(role => <RoleComp key={role.id} role={role} onClick={() => {
|
||||
{equippedRoles.map(role => <RoleComp key={role.id} role={role} onClick={(event) => {
|
||||
event.preventDefault();
|
||||
roleDeselected(role);
|
||||
}} />)}
|
||||
</Dropdown.Header>
|
||||
@ -233,20 +234,23 @@ const Users = () => {
|
||||
|
||||
const [users, setUsers] = useState<APIUser[]>([]);
|
||||
const [roles, updateRoles] = useState<Role[]>([]);
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const [page, setPage] = useState(1);
|
||||
const [pages, setPages] = useState(1);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const result = await get('/api/users', { page });
|
||||
if (result.success) {
|
||||
if (result.success && result.data) {
|
||||
setError(null);
|
||||
setUsers(result.data as APIUser[]);
|
||||
setUsers(result.data.users as APIUser[]);
|
||||
setPages(result.data.pages);
|
||||
} else setError(result.message || 'Unknown error');
|
||||
setLoading(false);
|
||||
const rolesResponse = await get('/api/roles');
|
||||
if (rolesResponse.success) updateRoles(rolesResponse.data as Role[]);
|
||||
const rolesResponse = await get('/api/roles', { all: true });
|
||||
if (rolesResponse.success && rolesResponse.data)
|
||||
updateRoles(rolesResponse.data.roles as Role[]);
|
||||
})();
|
||||
}, [page]);
|
||||
|
||||
@ -272,7 +276,7 @@ const Users = () => {
|
||||
itemKeys={['name', 'id']} />)}
|
||||
</Table>
|
||||
|
||||
<PageButtons {...{ page, setPage, length: users.length }} />
|
||||
<PageButtons {...{ page, setPage, pages }} />
|
||||
</div>
|
||||
|
||||
<CreateUserField className="col-6-lg col-12" />
|
||||
|
@ -31,7 +31,7 @@ const ClickDetector = ({ children, callback }: {children: React.ReactNode, callb
|
||||
alerter(wrapperRef, callback);
|
||||
|
||||
return (
|
||||
<div ref={wrapperRef}>
|
||||
<div className='click-detector' ref={wrapperRef}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
@ -63,7 +63,7 @@ const parseResponse = async (response: Response): Promise<Res> => {
|
||||
const data = await response.json();
|
||||
return {...base, data};
|
||||
}
|
||||
return { ...base, message: await response.text() };
|
||||
return { ...base, message: (await response.text() || response.statusText) };
|
||||
};
|
||||
|
||||
export const post = async (url: string, body?: object | string, opts: ReqOptions = {}) => {
|
||||
@ -107,9 +107,9 @@ export const patch = async (url: string, body: object | string, opts: RequestOpt
|
||||
|
||||
};
|
||||
|
||||
export const get = async (url: string, params?: {[key: string]: string | number}) => {
|
||||
export const get = async (url: string, params?: {[key: string]: boolean | string | number | string[] | number[]}) => {
|
||||
if (params) url += '?' + Object.entries(params)
|
||||
.map(([key, val]) => `${key}=${val}`)
|
||||
.map(([key, val]) => `${key}=${val instanceof Array ? val.join(',') : val}`)
|
||||
.join('&');
|
||||
const response = await fetch(url);
|
||||
return parseResponse(response);
|
||||
|
Loading…
Reference in New Issue
Block a user