import React, { useEffect, useRef, useState } from 'react'; import Modal, { RequiredModalProps } from '@/components/elements/Modal'; import { Field, Form, Formik, FormikHelpers, useFormikContext } from 'formik'; import { Actions, useStoreActions, useStoreState } from 'easy-peasy'; import { object, string } from 'yup'; import debounce from 'debounce'; import FormikFieldWrapper from '@/components/elements/FormikFieldWrapper'; import InputSpinner from '@/components/elements/InputSpinner'; import getServers from '@/api/getServers'; import { Server } from '@/api/server/getServer'; import { ApplicationStore } from '@/state'; import { httpErrorToHuman } from '@/api/http'; import { Link } from 'react-router-dom'; import styled from 'styled-components/macro'; import tw from 'twin.macro'; import Input from '@/components/elements/Input'; type Props = RequiredModalProps; interface Values { term: string; } const ServerResult = styled(Link)` ${tw`flex items-center bg-neutral-900 p-4 rounded border-l-4 border-neutral-900 no-underline transition-all duration-150`}; &:hover { ${tw`shadow border-cyan-500`}; } &:not(:last-of-type) { ${tw`mb-2`}; } `; const SearchWatcher = () => { const { values, submitForm } = useFormikContext<Values>(); useEffect(() => { if (values.term.length >= 3) { submitForm(); } }, [ values.term ]); return null; }; export default ({ ...props }: Props) => { const ref = useRef<HTMLInputElement>(null); const [ loading, setLoading ] = useState(false); const [ servers, setServers ] = useState<Server[]>([]); const isAdmin = useStoreState(state => state.user.data!.rootAdmin); const { addError, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes); const search = debounce(({ term }: Values, { setSubmitting }: FormikHelpers<Values>) => { setLoading(true); setSubmitting(false); clearFlashes('search'); getServers({ query: term }) .then(servers => setServers(servers.items.filter((_, index) => index < 5))) .catch(error => { console.error(error); addError({ key: 'search', message: httpErrorToHuman(error) }); }) .then(() => setLoading(false)); }, 500); useEffect(() => { if (props.visible) { setTimeout(() => ref.current?.focus(), 250); } }, [ props.visible ]); return ( <Formik onSubmit={search} validationSchema={object().shape({ term: string() .min(3, 'Please enter at least three characters to begin searching.') .required('A search term must be provided.'), })} initialValues={{ term: '' } as Values} > <Modal {...props}> <Form> <FormikFieldWrapper name={'term'} label={'Search term'} description={ isAdmin ? 'Enter a server name, user email, or uuid to begin searching.' : 'Enter a server name to begin searching.' } > <SearchWatcher/> <InputSpinner visible={loading}> <Field as={Input} innerRef={ref} name={'term'}/> </InputSpinner> </FormikFieldWrapper> </Form> {servers.length > 0 && <div css={tw`mt-6`}> { servers.map(server => ( <ServerResult key={server.uuid} to={`/server/${server.id}`} onClick={() => props.onDismissed()} > <div> <p css={tw`text-sm`}>{server.name}</p> <p css={tw`mt-1 text-xs text-neutral-400`}> { server.allocations.filter(alloc => alloc.isDefault).map(allocation => ( <span key={allocation.ip + allocation.port.toString()}>{allocation.alias || allocation.ip}:{allocation.port}</span> )) } </p> </div> <div css={tw`flex-1 text-right`}> <span css={tw`text-xs py-1 px-2 bg-cyan-800 text-cyan-100 rounded`}> {server.node} </span> </div> </ServerResult> )) } </div> } </Modal> </Formik> ); };