ui(admin): add search and sort to ServersContainer
This commit is contained in:
parent
ae88a01bea
commit
bca2338863
16 changed files with 1097 additions and 1381 deletions
|
@ -1,26 +1,33 @@
|
|||
import React from 'react';
|
||||
import Input from '@/components/elements/Input';
|
||||
import InputSpinner from '@/components/elements/InputSpinner';
|
||||
import { debounce } from 'debounce';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { TableCheckbox } from '@/components/admin/AdminCheckbox';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import styled from 'styled-components/macro';
|
||||
import tw from 'twin.macro';
|
||||
import { PaginatedResult } from '@/api/http';
|
||||
import { PaginatedResult, PaginationDataSet } from '@/api/http';
|
||||
|
||||
export const TableHeader = ({ name }: { name?: string }) => {
|
||||
export const TableHeader = ({ name, onClick, direction }: { name?: string, onClick?: (e: React.MouseEvent) => void, direction?: number | null }) => {
|
||||
if (!name) {
|
||||
return <th css={tw`px-6 py-2`}/>;
|
||||
}
|
||||
|
||||
return (
|
||||
<th css={tw`px-6 py-2`}>
|
||||
<th css={tw`px-6 py-2`} onClick={onClick}>
|
||||
<span css={tw`flex flex-row items-center cursor-pointer`}>
|
||||
<span css={tw`text-xs font-medium tracking-wider uppercase text-neutral-300 whitespace-nowrap`}>{name}</span>
|
||||
<span css={tw`text-xs font-medium tracking-wider uppercase text-neutral-300 whitespace-nowrap select-none`}>{name}</span>
|
||||
|
||||
<div css={tw`ml-1`}>
|
||||
<svg fill="none" viewBox="0 0 20 20" css={tw`w-4 h-4 text-neutral-400`}>
|
||||
<path stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" d="M13 7L10 4L7 7"/>
|
||||
<path stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" d="M7 13L10 16L13 13"/>
|
||||
</svg>
|
||||
</div>
|
||||
{direction !== undefined ?
|
||||
<div css={tw`ml-1`}>
|
||||
<svg fill="none" viewBox="0 0 20 20" css={tw`w-4 h-4 text-neutral-400`}>
|
||||
{(direction === null || direction === 1) ? <path stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" d="M13 7L10 4L7 7"/> : null}
|
||||
{(direction === null || direction === 2) ? <path stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" d="M7 13L10 16L13 13"/> : null}
|
||||
</svg>
|
||||
</div>
|
||||
:
|
||||
null
|
||||
}
|
||||
</span>
|
||||
</th>
|
||||
);
|
||||
|
@ -54,7 +61,7 @@ export const TableRow = ({ children }: { children: React.ReactNode }) => {
|
|||
};
|
||||
|
||||
interface Props<T> {
|
||||
data: PaginatedResult<T>;
|
||||
data?: PaginatedResult<T>;
|
||||
onPageSelect: (page: number) => void;
|
||||
|
||||
children: React.ReactNode;
|
||||
|
@ -78,7 +85,20 @@ const PaginationArrow = styled.button`
|
|||
}
|
||||
`;
|
||||
|
||||
export function Pagination<T> ({ data: { pagination }, onPageSelect, children }: Props<T>) {
|
||||
export function Pagination<T> ({ data, onPageSelect, children }: Props<T>) {
|
||||
let pagination: PaginationDataSet;
|
||||
if (data === undefined) {
|
||||
pagination = {
|
||||
total: 0,
|
||||
count: 0,
|
||||
perPage: 0,
|
||||
currentPage: 1,
|
||||
totalPages: 1,
|
||||
};
|
||||
} else {
|
||||
pagination = data.pagination;
|
||||
}
|
||||
|
||||
const setPage = (page: number) => {
|
||||
if (page < 1 || page > pagination.totalPages) {
|
||||
return;
|
||||
|
@ -173,11 +193,27 @@ export const NoItems = () => {
|
|||
interface Params {
|
||||
checked: boolean;
|
||||
onSelectAllClick: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
onSearch?: (query: string) => Promise<void>;
|
||||
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const ContentWrapper = ({ checked, onSelectAllClick, children }: Params) => {
|
||||
export const ContentWrapper = ({ checked, onSelectAllClick, onSearch, children }: Params) => {
|
||||
const [ loading, setLoading ] = useState(false);
|
||||
const [ inputText, setInputText ] = useState('');
|
||||
|
||||
const search = useCallback(
|
||||
debounce((query: string) => {
|
||||
if (onSearch === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
onSearch(query).then(() => setLoading(false));
|
||||
}, 200),
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div css={tw`flex flex-row items-center h-12 px-6`}>
|
||||
|
@ -194,15 +230,19 @@ export const ContentWrapper = ({ checked, onSelectAllClick, children }: Params)
|
|||
</svg>
|
||||
</div>
|
||||
|
||||
{/* <div css={tw`flex flex-row items-center px-2 py-1 ml-auto rounded cursor-pointer bg-neutral-600`}>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor" css={tw`w-6 h-6 text-neutral-300`}>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z"/>
|
||||
</svg>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" css={tw`w-4 h-4 ml-1 text-neutral-200`}>
|
||||
<path clipRule="evenodd" fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z"/>
|
||||
</svg>
|
||||
</div> */}
|
||||
<div css={tw`flex flex-row items-center ml-auto`}>
|
||||
<InputSpinner visible={loading}>
|
||||
<Input
|
||||
value={inputText}
|
||||
css={tw`h-8`}
|
||||
placeholder="Search..."
|
||||
onChange={e => {
|
||||
setInputText(e.currentTarget.value);
|
||||
search(e.currentTarget.value);
|
||||
}}
|
||||
/>
|
||||
</InputSpinner>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{children}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import CopyOnClick from '@/components/elements/CopyOnClick';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import getDatabases, { Context as DatabasesContext } from '@/api/admin/databases/getDatabases';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
|
@ -10,6 +9,7 @@ import AdminContentBlock from '@/components/admin/AdminContentBlock';
|
|||
import AdminCheckbox from '@/components/admin/AdminCheckbox';
|
||||
import AdminTable, { TableBody, TableHead, TableHeader, TableRow, Pagination, Loading, NoItems, ContentWrapper } from '@/components/admin/AdminTable';
|
||||
import Button from '@/components/elements/Button';
|
||||
import CopyOnClick from '@/components/elements/CopyOnClick';
|
||||
|
||||
const RowCheckbox = ({ id }: { id: number}) => {
|
||||
const isChecked = AdminContext.useStoreState(state => state.databases.selectedDatabases.indexOf(id) >= 0);
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
import LocationDeleteButton from '@/components/admin/locations/LocationDeleteButton';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
import tw from 'twin.macro';
|
||||
import { useHistory, useRouteMatch } from 'react-router-dom';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import { action, Action, Actions, createContextStore, useStoreActions } from 'easy-peasy';
|
||||
import { Location } from '@/api/admin/locations/getLocations';
|
||||
import getLocation from '@/api/admin/locations/getLocation';
|
||||
|
@ -15,7 +17,6 @@ import Field from '@/components/elements/Field';
|
|||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
import { Form, Formik, FormikHelpers } from 'formik';
|
||||
import updateLocation from '@/api/admin/locations/updateLocation';
|
||||
import LocationDeleteButton from '@/components/admin/locations/LocationDeleteButton';
|
||||
|
||||
interface ctx {
|
||||
location: Location | undefined;
|
||||
|
@ -99,12 +100,12 @@ const EditInformationContainer = () => {
|
|||
</div>
|
||||
|
||||
<div css={tw`w-full flex flex-row items-center mt-6`}>
|
||||
<div css={tw`flex`}>
|
||||
<LocationDeleteButton
|
||||
locationId={location.id}
|
||||
onDeleted={() => history.push('/admin/locations')}
|
||||
/>
|
||||
</div>
|
||||
<div css={tw`flex`}>
|
||||
<LocationDeleteButton
|
||||
locationId={location.id}
|
||||
onDeleted={() => history.push('/admin/locations')}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div css={tw`flex ml-auto`}>
|
||||
<Button type={'submit'} disabled={isSubmitting || !isValid}>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import tw from 'twin.macro';
|
||||
import { useHistory, useRouteMatch } from 'react-router-dom';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import { action, Action, Actions, createContextStore, useStoreActions } from 'easy-peasy';
|
||||
import { Mount } from '@/api/admin/mounts/getMounts';
|
||||
import getMount from '@/api/admin/mounts/getMount';
|
||||
|
@ -15,7 +15,6 @@ import Button from '@/components/elements/Button';
|
|||
import Field from '@/components/elements/Field';
|
||||
import SpinnerOverlay from '@/components/elements/SpinnerOverlay';
|
||||
import { Field as FormikField, Form, Formik, FormikHelpers } from 'formik';
|
||||
import MountDeleteButton from '@/components/admin/mounts/MountDeleteButton';
|
||||
import Label from '@/components/elements/Label';
|
||||
|
||||
interface ctx {
|
||||
|
@ -41,8 +40,6 @@ interface Values {
|
|||
}
|
||||
|
||||
const EditInformationContainer = () => {
|
||||
const history = useHistory();
|
||||
|
||||
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
|
||||
const mount = Context.useStoreState(state => state.mount);
|
||||
|
@ -184,10 +181,10 @@ const EditInformationContainer = () => {
|
|||
|
||||
<div css={tw`w-full flex flex-row items-center mt-6`}>
|
||||
<div css={tw`flex`}>
|
||||
<MountDeleteButton
|
||||
mountId={mount.id}
|
||||
onDeleted={() => history.push('/admin/mounts')}
|
||||
/>
|
||||
{/* <MountDeleteButton */}
|
||||
{/* mountId={mount.id} */}
|
||||
{/* onDeleted={() => history.push('/admin/mounts')} */}
|
||||
{/* /> */}
|
||||
</div>
|
||||
|
||||
<div css={tw`flex ml-auto`}>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import { NavLink, useHistory, useRouteMatch } from 'react-router-dom';
|
||||
import { NavLink, useRouteMatch } from 'react-router-dom';
|
||||
import tw from 'twin.macro';
|
||||
import AdminContentBlock from '@/components/admin/AdminContentBlock';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
|
@ -20,7 +20,6 @@ import AdminTable, { ContentWrapper, NoItems, TableBody, TableHead, TableHeader,
|
|||
import CopyOnClick from '@/components/elements/CopyOnClick';
|
||||
import Input from '@/components/elements/Input';
|
||||
import Label from '@/components/elements/Label';
|
||||
import NestDeleteButton from '@/components/admin/nests/NestDeleteButton';
|
||||
|
||||
interface ctx {
|
||||
nest: Nest | undefined;
|
||||
|
@ -61,8 +60,6 @@ interface Values {
|
|||
}
|
||||
|
||||
const EditInformationContainer = () => {
|
||||
const history = useHistory();
|
||||
|
||||
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
|
||||
const nest = Context.useStoreState(state => state.nest);
|
||||
|
@ -125,10 +122,10 @@ const EditInformationContainer = () => {
|
|||
|
||||
<div css={tw`w-full flex flex-row items-center mt-6`}>
|
||||
<div css={tw`flex`}>
|
||||
<NestDeleteButton
|
||||
nestId={nest.id}
|
||||
onDeleted={() => history.push('/admin/nests')}
|
||||
/>
|
||||
{/* <NestDeleteButton */}
|
||||
{/* nestId={nest.id} */}
|
||||
{/* onDeleted={() => history.push('/admin/nests')} */}
|
||||
{/* /> */}
|
||||
</div>
|
||||
|
||||
<div css={tw`flex ml-auto`}>
|
||||
|
|
|
@ -98,7 +98,7 @@ export default () => {
|
|||
<TableBody>
|
||||
{
|
||||
roles.map(role => (
|
||||
<TableRow key={role.id}>
|
||||
<TableRow key={role.id} css={role.id === roles[roles.length - 1].id ? tw`rounded-b-lg` : undefined}>
|
||||
<td css={tw`pl-6`}>
|
||||
<RowCheckbox id={role.id}/>
|
||||
</td>
|
||||
|
|
|
@ -1,85 +0,0 @@
|
|||
import React, { useEffect, useState } from 'react';
|
||||
import tw from 'twin.macro';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import { action, Action, Actions, createContextStore, useStoreActions } from 'easy-peasy';
|
||||
import { Server } from '@/api/admin/servers/getServers';
|
||||
import getServer from '@/api/admin/servers/getServer';
|
||||
import AdminContentBlock from '@/components/admin/AdminContentBlock';
|
||||
import Spinner from '@/components/elements/Spinner';
|
||||
import FlashMessageRender from '@/components/FlashMessageRender';
|
||||
import { ApplicationStore } from '@/state';
|
||||
|
||||
interface ctx {
|
||||
server: Server | undefined;
|
||||
setServer: Action<ctx, Server | undefined>;
|
||||
}
|
||||
|
||||
export const Context = createContextStore<ctx>({
|
||||
server: undefined,
|
||||
|
||||
setServer: action((state, payload) => {
|
||||
state.server = payload;
|
||||
}),
|
||||
});
|
||||
|
||||
const ServerEditContainer = () => {
|
||||
const match = useRouteMatch<{ id?: string }>();
|
||||
|
||||
const { clearFlashes, clearAndAddHttpError } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
|
||||
const [ loading, setLoading ] = useState(true);
|
||||
|
||||
const server = Context.useStoreState(state => state.server);
|
||||
const setServer = Context.useStoreActions(actions => actions.setServer);
|
||||
|
||||
useEffect(() => {
|
||||
clearFlashes('server');
|
||||
|
||||
getServer(Number(match.params?.id), [])
|
||||
.then(server => setServer(server))
|
||||
.catch(error => {
|
||||
console.error(error);
|
||||
clearAndAddHttpError({ key: 'server', error });
|
||||
})
|
||||
.then(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
if (loading || server === undefined) {
|
||||
return (
|
||||
<AdminContentBlock>
|
||||
<FlashMessageRender byKey={'server'} css={tw`mb-4`}/>
|
||||
|
||||
<div css={tw`w-full flex flex-col items-center justify-center`} style={{ height: '24rem' }}>
|
||||
<Spinner size={'base'}/>
|
||||
</div>
|
||||
</AdminContentBlock>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminContentBlock title={'Server - ' + server.name}>
|
||||
<div css={tw`w-full flex flex-row items-center mb-8`}>
|
||||
<div css={tw`flex flex-col flex-shrink`} style={{ minWidth: '0' }}>
|
||||
<h2 css={tw`text-2xl text-neutral-50 font-header font-medium`}>{server.name}</h2>
|
||||
{
|
||||
(server.description || '').length < 1 ?
|
||||
<p css={tw`text-base text-neutral-400`}>
|
||||
<span css={tw`italic`}>No description</span>
|
||||
</p>
|
||||
:
|
||||
<p css={tw`text-base text-neutral-400 whitespace-nowrap overflow-ellipsis overflow-hidden`}>{server.description}</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FlashMessageRender byKey={'server'} css={tw`mb-4`}/>
|
||||
</AdminContentBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export default () => {
|
||||
return (
|
||||
<Context.Provider>
|
||||
<ServerEditContainer/>
|
||||
</Context.Provider>
|
||||
);
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue