Finish support for creating databases in the UI

This commit is contained in:
Dane Everitt 2019-07-16 21:43:11 -07:00
parent 61dc86421d
commit 1f763dc155
No known key found for this signature in database
GPG key ID: EEA66103B3D71F53
7 changed files with 136 additions and 12 deletions

View file

@ -0,0 +1,15 @@
import { rawDataToServerDatabase, ServerDatabase } from '@/api/server/getServerDatabases';
import http from '@/api/http';
export default (uuid: string, data: { connectionsFrom: string; databaseName: string }): Promise<ServerDatabase> => {
return new Promise((resolve, reject) => {
http.post(`/api/client/servers/${uuid}/databases`, {
database: data.databaseName,
remote: data.connectionsFrom,
}, {
params: { include: 'password' },
})
.then(response => resolve(rawDataToServerDatabase(response.data.attributes)))
.catch(reject);
});
};

View file

@ -2,6 +2,7 @@ import React, { useEffect, useState } from 'react';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faTimes } from '@fortawesome/free-solid-svg-icons/faTimes'; import { faTimes } from '@fortawesome/free-solid-svg-icons/faTimes';
import { CSSTransition } from 'react-transition-group'; import { CSSTransition } from 'react-transition-group';
import Spinner from '@/components/elements/Spinner';
interface Props { interface Props {
visible: boolean; visible: boolean;
@ -9,7 +10,8 @@ interface Props {
dismissable?: boolean; dismissable?: boolean;
closeOnEscape?: boolean; closeOnEscape?: boolean;
closeOnBackground?: boolean; closeOnBackground?: boolean;
children: React.ReactChild; showSpinnerOverlay?: boolean;
children: React.ReactNode;
} }
export default (props: Props) => { export default (props: Props) => {
@ -51,6 +53,14 @@ export default (props: Props) => {
<FontAwesomeIcon icon={faTimes}/> <FontAwesomeIcon icon={faTimes}/>
</div> </div>
} }
{props.showSpinnerOverlay &&
<div
className={'absolute w-full h-full rounded flex items-center justify-center'}
style={{ background: 'hsla(211, 10%, 53%, 0.25)' }}
>
<Spinner large={false}/>
</div>
}
<div className={'modal-content p-6'}> <div className={'modal-content p-6'}>
{props.children} {props.children}
</div> </div>

View file

@ -1,18 +1,112 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import { ServerDatabase } from '@/api/server/getServerDatabases'; import { ServerDatabase } from '@/api/server/getServerDatabases';
import Modal from '@/components/elements/Modal'; import Modal from '@/components/elements/Modal';
import { Form, Formik, FormikActions } from 'formik';
import Field from '@/components/elements/Field';
import { object, string } from 'yup';
import createServerDatabase from '@/api/server/createServerDatabase';
import { ServerContext } from '@/state/server';
import { Actions, useStoreActions } from 'easy-peasy';
import { ApplicationStore } from '@/state';
import { httpErrorToHuman } from '@/api/http';
import FlashMessageRender from '@/components/FlashMessageRender';
interface Values {
databaseName: string;
connectionsFrom: string;
}
const schema = object().shape({
databaseName: string()
.required('A database name must be provided.')
.min(5, 'Database name must be at least 5 characters.')
.max(64, 'Database name must not exceed 64 characters.')
.matches(/^[A-Za-z0-9_\-.]{5,64}$/, 'Database name should only contain alphanumeric characters, underscores, dashes, and/or periods.'),
connectionsFrom: string()
.required('A connection value must be provided.')
.matches(/^([1-9]{1,3}|%)(\.([0-9]{1,3}|%))?(\.([0-9]{1,3}|%))?(\.([0-9]{1,3}|%))?$/, 'A valid connection address must be provided.'),
});
export default ({ onCreated }: { onCreated: (database: ServerDatabase) => void }) => { export default ({ onCreated }: { onCreated: (database: ServerDatabase) => void }) => {
const [ visible, setVisible ] = useState(false); const [ visible, setVisible ] = useState(false);
const { addFlash, clearFlashes } = useStoreActions((actions: Actions<ApplicationStore>) => actions.flashes);
const server = ServerContext.useStoreState(state => state.server.data!);
const submit = (values: Values, { setSubmitting }: FormikActions<Values>) => {
clearFlashes();
createServerDatabase(server.uuid, { ...values })
.then(database => {
onCreated(database);
setVisible(false);
})
.catch(error => {
console.log(error);
addFlash({
key: 'create-database-modal',
type: 'error',
title: 'Error',
message: httpErrorToHuman(error),
});
})
.then(() => setSubmitting(false));
};
return ( return (
<React.Fragment> <React.Fragment>
<Modal visible={visible} onDismissed={() => setVisible(false)}> <Formik
<p>Testing</p> onSubmit={submit}
</Modal> initialValues={{ databaseName: '', connectionsFrom: '%' }}
<button className={'btn btn-primary btn-lg'} onClick={() => setVisible(true)}> validationSchema={schema}
>
{
({ isSubmitting, resetForm }) => (
<Modal
visible={visible}
dismissable={!isSubmitting}
showSpinnerOverlay={isSubmitting}
onDismissed={() => {
resetForm();
setVisible(false);
}}
>
<FlashMessageRender byKey={'create-database-modal'}/>
<h3 className={'mb-6'}>Create new database</h3>
<Form className={'m-0'}>
<Field
type={'string'}
id={'database_name'}
name={'databaseName'}
label={'Database Name'}
description={'A descriptive name for your database instance.'}
/>
<div className={'mt-6'}>
<Field
type={'string'}
id={'connections_from'}
name={'connectionsFrom'}
label={'Connections From'}
description={'Where connections should be allowed from. Use % for wildcards.'}
/>
</div>
<div className={'mt-6 text-right'}>
<button
className={'btn btn-sm btn-secondary mr-2'}
onClick={() => setVisible(false)}
>
Cancel
</button>
<button className={'btn btn-sm btn-primary'} type={'submit'}>
Create Database Create Database
</button> </button>
</div>
</Form>
</Modal>
)
}
</Formik>
<button className={'btn btn-primary btn-lg'} onClick={() => setVisible(true)}>
New Database
</button>
</React.Fragment> </React.Fragment>
); );
}; };

View file

@ -4,10 +4,11 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faDatabase } from '@fortawesome/free-solid-svg-icons/faDatabase'; import { faDatabase } from '@fortawesome/free-solid-svg-icons/faDatabase';
import { faTrashAlt } from '@fortawesome/free-solid-svg-icons/faTrashAlt'; import { faTrashAlt } from '@fortawesome/free-solid-svg-icons/faTrashAlt';
import { faEye } from '@fortawesome/free-solid-svg-icons/faEye'; import { faEye } from '@fortawesome/free-solid-svg-icons/faEye';
import classNames from 'classnames';
export default ({ database }: { database: ServerDatabase }) => { export default ({ database, className }: { database: ServerDatabase; className?: string }) => {
return ( return (
<div className={'grey-row-box no-hover'}> <div className={classNames('grey-row-box no-hover', className)}>
<div className={'icon'}> <div className={'icon'}>
<FontAwesomeIcon icon={faDatabase}/> <FontAwesomeIcon icon={faDatabase}/>
</div> </div>

View file

@ -40,7 +40,11 @@ export default () => {
<CSSTransition classNames={'fade'} timeout={250}> <CSSTransition classNames={'fade'} timeout={250}>
<React.Fragment> <React.Fragment>
{databases.length > 0 ? {databases.length > 0 ?
databases.map(database => <DatabaseRow key={database.id} database={database}/>) databases.map((database, index) => <DatabaseRow
key={database.id}
database={database}
className={index > 0 ? 'mt-1' : undefined}
/>)
: :
<p className={'text-center text-sm text-neutral-200'}> <p className={'text-center text-sm text-neutral-200'}>
It looks like you have no databases. Click the button below to create one now. It looks like you have no databases. Click the button below to create one now.

View file

@ -1,6 +1,6 @@
.modal-mask { .modal-mask {
@apply .fixed .pin .z-50 .overflow-auto .flex; @apply .fixed .pin .z-50 .overflow-auto .flex;
background: rgba(0, 0, 0, 0.7); background: rgba(0, 0, 0, 0.70);
transition: opacity 250ms ease; transition: opacity 250ms ease;
& > .modal-container { & > .modal-container {
@ -22,7 +22,7 @@
} }
& > .modal-content { & > .modal-content {
@apply .bg-neutral-900 .rounded .shadow-md; @apply .bg-neutral-800 .rounded .shadow-md;
transition: all 250ms ease; transition: all 250ms ease;
} }

View file

@ -135,7 +135,7 @@ module.exports = {
}, },
plugins: plugins, plugins: plugins,
optimization: { optimization: {
minimize: true, minimize: isProduction,
minimizer: [ minimizer: [
new TerserPlugin({ new TerserPlugin({
cache: true, cache: true,