Skip to content

Commit

Permalink
UI: Create Mirror And Mirror Overview Improvements (#890)
Browse files Browse the repository at this point in the history
## UI Improvement Features
These are some features which I thought would be nice to get in. At the
end of the day, these are just proposals from my end.

### Mirror Create Pages: Error Toasts And Floating Button
We now show errors as toasts and have the Create Mirror button with a
fixed position on the bottom-right. Users now don't have to do a lot of
scrolling up and down to look at the error message, come back, and click
create.

<img width="1461" alt="Screenshot 2023-12-23 at 10 26 14 PM"
src="https://github.com/PeerDB-io/peerdb/assets/65964360/a21c2b23-bddc-4c56-91df-962f02b64589">

### QRep Mirror: Upsert Columns
Selection of Unique Key Columns for QRep Upsert mode now looks like
this, saving users from having to type out columns. Also added
validation for the columns being an empty array.

<img width="1461" alt="Screenshot 2023-12-23 at 9 49 35 PM"
src="https://github.com/PeerDB-io/peerdb/assets/65964360/42b6beb9-afb9-44aa-a55f-8973958ec30f">

### Better Tabs UI for Mirror Overview
I thought the tabs we have there look unpolished so used Tremor to come
up with this. This also achieves significant code reduction in that
file.

<img width="1726" alt="Screenshot 2023-12-23 at 11 37 58 PM"
src="https://github.com/PeerDB-io/peerdb/assets/65964360/23b623ec-82c0-4df4-9e9d-32560a6c0d91">

### Wiring Status in Mirror Overview Page
Wires in the Status we show in the mirror overview page. This is a
follow-up to #883

<img width="601" alt="Screenshot 2023-12-23 at 10 28 23 PM"
src="https://github.com/PeerDB-io/peerdb/assets/65964360/9fe08a47-c774-496b-8417-02a27fcee034">

### Others
- Removes 'Authentication failed' message in login landing page.
- Makes the source-destination table list in Mirror Overview page have
scrollable height and sticky headers
- Error table now has time column before message column and the rows are
sorted by timestamp (latest first)

---------

Co-authored-by: Kaushik Iska <[email protected]>
  • Loading branch information
Amogh-Bharadwaj and iskakaushik authored Dec 25, 2023
1 parent d407f9e commit ac5bbb2
Show file tree
Hide file tree
Showing 14 changed files with 308 additions and 233 deletions.
4 changes: 1 addition & 3 deletions ui/app/login/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,7 @@ export default function Login() {
const searchParams = useSearchParams();
const [pass, setPass] = useState('');
const [show, setShow] = useState(false);
const [error, setError] = useState(() =>
searchParams.has('reject') ? 'Authentication failed, please login' : ''
);
const [error, setError] = useState(() => '');
const login = () => {
fetch('/api/login', {
method: 'POST',
Expand Down
75 changes: 27 additions & 48 deletions ui/app/mirrors/create/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,45 +73,33 @@ const validateCDCFields = (
}
| undefined
)[],
setMsg: Dispatch<SetStateAction<{ ok: boolean; msg: string }>>,
config: CDCConfig
): boolean => {
): string | undefined => {
let validationErr: string | undefined;
const tablesValidity = tableMappingSchema.safeParse(tableMapping);
if (!tablesValidity.success) {
validationErr = tablesValidity.error.issues[0].message;
setMsg({ ok: false, msg: validationErr });
return false;
}
const configValidity = cdcSchema.safeParse(config);
if (!configValidity.success) {
validationErr = configValidity.error.issues[0].message;
setMsg({ ok: false, msg: validationErr });
return false;
}
setMsg({ ok: true, msg: '' });
return true;
return validationErr;
};

const validateQRepFields = (
query: string,
setMsg: Dispatch<SetStateAction<{ ok: boolean; msg: string }>>,
config: QRepConfig
): boolean => {
): string | undefined => {
if (query.length < 5) {
setMsg({ ok: false, msg: 'Query is invalid' });
return false;
return 'Query is invalid';
}

let validationErr: string | undefined;
const configValidity = qrepSchema.safeParse(config);
if (!configValidity.success) {
validationErr = configValidity.error.issues[0].message;
setMsg({ ok: false, msg: validationErr });
return false;
}
setMsg({ ok: true, msg: '' });
return true;
return validationErr;
};

interface TableMapping {
Expand Down Expand Up @@ -140,25 +128,23 @@ export const handleCreateCDC = async (
flowJobName: string,
rows: TableMapRow[],
config: CDCConfig,
setMsg: Dispatch<
SetStateAction<{
ok: boolean;
msg: string;
}>
>,
notify: (msg: string) => void,
setLoading: Dispatch<SetStateAction<boolean>>,
route: RouteCallback
) => {
const flowNameValid = flowNameSchema.safeParse(flowJobName);
if (!flowNameValid.success) {
const flowNameErr = flowNameValid.error.issues[0].message;
setMsg({ ok: false, msg: flowNameErr });
notify(flowNameErr);
return;
}

const tableNameMapping = reformattedTableMapping(rows);
const isValid = validateCDCFields(tableNameMapping, setMsg, config);
if (!isValid) return;
const fieldErr = validateCDCFields(tableNameMapping, config);
if (fieldErr) {
notify(fieldErr);
return;
}

config['tableMappings'] = tableNameMapping as TableMapping[];
config['flowJobName'] = flowJobName;
Expand All @@ -172,10 +158,7 @@ export const handleCreateCDC = async (
}

if (config.doInitialCopy == false && config.initialCopyOnly == true) {
setMsg({
ok: false,
msg: 'Initial Copy Only cannot be true if Initial Copy is false.',
});
notify('Initial Copy Only cannot be true if Initial Copy is false.');
return;
}

Expand All @@ -187,11 +170,11 @@ export const handleCreateCDC = async (
}),
}).then((res) => res.json());
if (!statusMessage.created) {
setMsg({ ok: false, msg: 'unable to create mirror.' });
notify('unable to create mirror.');
setLoading(false);
return;
}
setMsg({ ok: true, msg: 'CDC Mirror created successfully' });
notify('CDC Mirror created successfully');
route();
setLoading(false);
};
Expand All @@ -209,20 +192,15 @@ export const handleCreateQRep = async (
flowJobName: string,
query: string,
config: QRepConfig,
setMsg: Dispatch<
SetStateAction<{
ok: boolean;
msg: string;
}>
>,
notify: (msg: string) => void,
setLoading: Dispatch<SetStateAction<boolean>>,
route: RouteCallback,
xmin?: boolean
) => {
const flowNameValid = flowNameSchema.safeParse(flowJobName);
if (!flowNameValid.success) {
const flowNameErr = flowNameValid.error.issues[0].message;
setMsg({ ok: false, msg: flowNameErr });
notify(flowNameErr);
return;
}

Expand All @@ -237,16 +215,17 @@ export const handleCreateQRep = async (

if (
config.writeMode?.writeType == QRepWriteType.QREP_WRITE_MODE_UPSERT &&
!config.writeMode?.upsertKeyColumns
(!config.writeMode?.upsertKeyColumns ||
config.writeMode?.upsertKeyColumns.length == 0)
) {
setMsg({
ok: false,
msg: 'For upsert mode, unique key columns cannot be empty.',
});
notify('For upsert mode, unique key columns cannot be empty.');
return;
}
const fieldErr = validateQRepFields(query, config);
if (fieldErr) {
notify(fieldErr);
return;
}
const isValid = validateQRepFields(query, setMsg, config);
if (!isValid) return;
config.flowJobName = flowJobName;
config.query = query;

Expand All @@ -267,11 +246,11 @@ export const handleCreateQRep = async (
}
).then((res) => res.json());
if (!statusMessage.created) {
setMsg({ ok: false, msg: 'unable to create mirror.' });
notify('unable to create mirror.');
setLoading(false);
return;
}
setMsg({ ok: true, msg: 'Query Replication Mirror created successfully' });
notify('Query Replication Mirror created successfully');
route();
setLoading(false);
};
Expand Down
3 changes: 2 additions & 1 deletion ui/app/mirrors/create/helpers/qrep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,9 @@ export const qrepSettings: MirrorSetting[] = [
writeMode: currWriteMode,
};
}),
tips: `Comma separated string column names. Needed when write mode is set to UPSERT.
tips: `Needed when write mode is set to UPSERT.
These columns need to be unique and are used for updates.`,
type: 'select',
},
{
label: 'Initial Copy Only',
Expand Down
93 changes: 46 additions & 47 deletions ui/app/mirrors/create/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@ import { RequiredIndicator } from '@/components/RequiredIndicator';
import { QRepConfig } from '@/grpc_generated/flow';
import { DBType, Peer } from '@/grpc_generated/peers';
import { Button } from '@/lib/Button';
import { ButtonGroup } from '@/lib/ButtonGroup';
import { Icon } from '@/lib/Icon';
import { Label } from '@/lib/Label';
import { RowWithSelect, RowWithTextField } from '@/lib/Layout';
import { Panel } from '@/lib/Panel';
import { TextField } from '@/lib/TextField';
import { Divider } from '@tremor/react';
import Image from 'next/image';
import Link from 'next/link';
import { useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import ReactSelect from 'react-select';
import { ToastContainer, toast } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { InfoPopover } from '../../../components/InfoPopover';
import { CDCConfig, TableMapRow } from '../../dto/MirrorsDTO';
import CDCConfigForm from './cdc/cdc';
Expand Down Expand Up @@ -47,22 +48,23 @@ function getPeerLabel(peer: Peer) {
);
}

const notifyErr = (errMsg: string) => {
toast.error(errMsg, {
position: toast.POSITION.BOTTOM_CENTER,
});
};
export default function CreateMirrors() {
const router = useRouter();
const [mirrorName, setMirrorName] = useState<string>('');
const [mirrorType, setMirrorType] = useState<string>('');
const [formMessage, setFormMessage] = useState<{ ok: boolean; msg: string }>({
ok: true,
msg: '',
});
const [loading, setLoading] = useState<boolean>(false);
const [config, setConfig] = useState<CDCConfig | QRepConfig>(blankCDCSetting);
const [peers, setPeers] = useState<Peer[]>([]);
const [rows, setRows] = useState<TableMapRow[]>([]);
const [qrepQuery, setQrepQuery] =
useState<string>(`-- Here's a sample template:
SELECT * FROM <table_name>
WHERE <watermark_column>
SELECT * FROM <table_name>
WHERE <watermark_column>
BETWEEN {{.start}} AND {{.end}}`);

useEffect(() => {
Expand Down Expand Up @@ -183,15 +185,7 @@ export default function CreateMirrors() {
Configuration
</Label>
)}
{!loading && formMessage.msg.length > 0 && (
<Label
colorName='lowContrast'
colorSet={formMessage.ok ? 'positive' : 'destructive'}
variant='subheadline'
>
{formMessage.msg}
</Label>
)}
{!loading && <ToastContainer />}
{mirrorType === '' ? (
<></>
) : mirrorType === 'CDC' ? (
Expand All @@ -213,36 +207,41 @@ export default function CreateMirrors() {
</Panel>
<Panel>
{mirrorType && (
<ButtonGroup className='justify-end'>
<Button as={Link} href='/mirrors'>
Cancel
</Button>
<Button
variant='normalSolid'
onClick={() =>
mirrorType === 'CDC'
? handleCreateCDC(
mirrorName,
rows,
config as CDCConfig,
setFormMessage,
setLoading,
listMirrorsPage
)
: handleCreateQRep(
mirrorName,
qrepQuery,
config as QRepConfig,
setFormMessage,
setLoading,
listMirrorsPage,
mirrorType === 'XMIN' // for handling xmin specific
)
}
>
Create Mirror
</Button>
</ButtonGroup>
<Button
style={{
position: 'fixed',
bottom: '5%',
right: '5%',
width: '10em',
height: '3em',
boxShadow: '0 2px 4px rgba(0, 0, 0, 0.2)',
borderRadius: '2em',
fontWeight: 'bold',
}}
variant='normalSolid'
onClick={() =>
mirrorType === 'CDC'
? handleCreateCDC(
mirrorName,
rows,
config as CDCConfig,
notifyErr,
setLoading,
listMirrorsPage
)
: handleCreateQRep(
mirrorName,
qrepQuery,
config as QRepConfig,
notifyErr,
setLoading,
listMirrorsPage,
mirrorType === 'XMIN' // for handling xmin specific
)
}
>
<Icon name='add' /> Create Mirror
</Button>
)}
</Panel>
</div>
Expand Down
13 changes: 9 additions & 4 deletions ui/app/mirrors/create/qrep/qrep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { MirrorSetter } from '../../../dto/MirrorsDTO';
import { defaultSyncMode } from '../cdc/cdc';
import { fetchAllTables, fetchColumns } from '../handlers';
import { MirrorSetting, blankQRepSetting } from '../helpers/common';
import UpsertColsDisplay from './upsertcols';

interface QRepConfigProps {
settings: MirrorSetting[];
Expand All @@ -29,10 +30,6 @@ interface QRepConfigProps {
xmin?: boolean;
}

const SyncModes = ['AVRO', 'Copy with Binary'].map((value) => ({
label: value,
value,
}));
const WriteModes = ['Append', 'Upsert', 'Overwrite'].map((value) => ({
label: value,
value,
Expand All @@ -50,6 +47,7 @@ export default function QRepConfigForm({
const [watermarkColumns, setWatermarkColumns] = useState<
{ value: string; label: string }[]
>([]);

const [loading, setLoading] = useState(false);

const handleChange = (val: string | boolean, setting: MirrorSetting) => {
Expand Down Expand Up @@ -220,6 +218,13 @@ export default function QRepConfigForm({
}
options={WriteModes}
/>
) : setting.label === 'Upsert Key Columns' ? (
<UpsertColsDisplay
columns={watermarkColumns}
loading={loading}
setter={setter}
setting={setting}
/>
) : (
<ReactSelect
placeholder={
Expand Down
Loading

0 comments on commit ac5bbb2

Please sign in to comment.