From 2a63521e6372c656f51f760ad9b2ccffe6864010 Mon Sep 17 00:00:00 2001 From: Noah Overcash Date: Mon, 29 Apr 2024 11:12:20 -0400 Subject: [PATCH] [MODFQMMGR-271] Support nested entity types in creator (#252) --- .../components/EntityTypeEditor.tsx | 56 ++++-- .../components/EntityTypeFieldEditor.tsx | 40 +++- .../components/EntityTypeManager.tsx | 7 +- .../components/PostgresConnector.tsx | 8 +- .../components/SourceEditor.tsx | 186 ++++++++++++++++++ entity-type-creator/pages/api/socket.ts | 2 + entity-type-creator/types.ts | 18 ++ entity-type-creator/utils/formatter.ts | 4 +- 8 files changed, 300 insertions(+), 21 deletions(-) create mode 100644 entity-type-creator/components/SourceEditor.tsx diff --git a/entity-type-creator/components/EntityTypeEditor.tsx b/entity-type-creator/components/EntityTypeEditor.tsx index d4a995a1..7e5111cf 100644 --- a/entity-type-creator/components/EntityTypeEditor.tsx +++ b/entity-type-creator/components/EntityTypeEditor.tsx @@ -2,12 +2,12 @@ import { formatSql } from '@/utils/sqlUtils'; import { PostgreSQL, sql } from '@codemirror/lang-sql'; import { Refresh } from '@mui/icons-material'; import { Alert, Button, Checkbox, FormControlLabel, Grid, IconButton, InputAdornment, TextField } from '@mui/material'; -import CodeMirror from '@uiw/react-codemirror'; import { useEffect, useMemo, useState } from 'react'; import { Socket } from 'socket.io-client'; import { v4 as uuid } from 'uuid'; import { DataTypeValue, EntityType } from '../types'; import EntityTypeFieldEditor from './EntityTypeFieldEditor'; +import SourceEditor from './SourceEditor'; export default function EntityTypeManager({ entityTypes, @@ -32,7 +32,7 @@ export default function EntityTypeManager({ useEffect(() => { setEntityType({ ...initialValues, - fromClause: formatSql(initialValues.fromClause), + sources: initialValues.sources ?? [], columns: initialValues.columns?.map((column) => ({ ...column, valueGetter: column.valueGetter ? formatSql(column.valueGetter) : undefined, @@ -54,7 +54,7 @@ export default function EntityTypeManager({ defaultSchema: 'TENANT_mod_fqm_manager', upperCaseKeywords: true, }), - [schema] + [schema], ); return ( @@ -114,6 +114,7 @@ export default function EntityTypeManager({ label="Root" control={ setEntityType({ ...entityType, root: e.target.checked })} /> @@ -123,6 +124,7 @@ export default function EntityTypeManager({ label="Private" control={ setEntityType({ ...entityType, private: e.target.checked })} /> @@ -164,14 +166,45 @@ export default function EntityTypeManager({ */} -
- FROM clause - setEntityType({ ...entityType, fromClause: value })} - extensions={[codeMirrorExtension]} - /> +
+ Sources + {entityType.sources?.map((source, i) => ( + + setEntityType({ + ...entityType, + sources: entityType.sources?.map((s, j) => (j === i ? newSource : s)), + }) + } + onRemove={() => + setEntityType({ ...entityType, sources: entityType.sources!.filter((_, j) => j !== i) }) + } + /> + ))} +
@@ -184,6 +217,7 @@ export default function EntityTypeManager({ parentName={entityType.name} entityType={entityType} entityTypes={entityTypes} + sources={entityType.sources ?? []} codeMirrorExtension={codeMirrorExtension} field={column} onChange={(newColumn) => diff --git a/entity-type-creator/components/EntityTypeFieldEditor.tsx b/entity-type-creator/components/EntityTypeFieldEditor.tsx index 2382e5ea..c0701698 100644 --- a/entity-type-creator/components/EntityTypeFieldEditor.tsx +++ b/entity-type-creator/components/EntityTypeFieldEditor.tsx @@ -1,7 +1,8 @@ -import { DataTypeValue, EntityType, EntityTypeField } from '@/types'; +import { DataTypeValue, EntityType, EntityTypeField, EntityTypeSource } from '@/types'; import { LanguageSupport } from '@codemirror/language'; import { ArrowDownward, ArrowUpward, Clear } from '@mui/icons-material'; import { + Autocomplete, Button, Checkbox, FormControl, @@ -12,6 +13,7 @@ import { MenuItem, Select, TextField, + Typography, } from '@mui/material'; import CodeMirror from '@uiw/react-codemirror'; import { useMemo } from 'react'; @@ -21,6 +23,7 @@ export default function EntityTypeFieldEditor({ parentName, entityType, entityTypes, + sources, codeMirrorExtension, field, onChange, @@ -36,6 +39,7 @@ export default function EntityTypeFieldEditor({ parentName: string; entityType: EntityType; entityTypes: EntityType[]; + sources: EntityTypeSource[]; codeMirrorExtension: LanguageSupport; field: EntityTypeField; onChange: (newColumn: EntityTypeField) => void; @@ -55,7 +59,7 @@ export default function EntityTypeFieldEditor({ - + - + + s.alias)} + renderInput={(params) => ( + + onChange({ + ...field, + sourceAlias: e.target.value, + }) + } + /> + )} + /> + + onChange({ ...field, queryable: e.target.checked })} /> @@ -80,6 +104,7 @@ export default function EntityTypeFieldEditor({ label="Visible by default" control={ onChange({ ...field, visibleByDefault: e.target.checked })} /> @@ -149,6 +174,7 @@ export default function EntityTypeFieldEditor({ label="Is ID column" control={ onChange({ ...field, visibleByDefault: e.target.checked })} /> @@ -479,6 +505,14 @@ export default function EntityTypeFieldEditor({ ), [field], )} + + + + + Use :sourceAlias to refer to the selected source {field.sourceAlias} + + + ))} + socket.emit('refresh-entity-types')}> + + {selected === 'new' && ( diff --git a/entity-type-creator/components/PostgresConnector.tsx b/entity-type-creator/components/PostgresConnector.tsx index 694d91ae..2c2a379c 100644 --- a/entity-type-creator/components/PostgresConnector.tsx +++ b/entity-type-creator/components/PostgresConnector.tsx @@ -11,11 +11,11 @@ export default function PostgresConnector({ const [open, setOpen] = useState(true); const [postgresConnection, setPostgresConnection] = useState({ - host: 'localhost', + host: 'rds-folio-perf-corsair.cluster-cdxr1geeeqbb.us-west-2.rds.amazonaws.com', port: 5432, - database: 'db', - user: 'postgres', - password: 'postgres', + database: 'folio', + user: 'folio', + password: 'postgres_password_123!', }); const [connectionState, setConnectionState] = useState({ connected: false, message: 'Waiting to connect...' }); diff --git a/entity-type-creator/components/SourceEditor.tsx b/entity-type-creator/components/SourceEditor.tsx new file mode 100644 index 00000000..7de92972 --- /dev/null +++ b/entity-type-creator/components/SourceEditor.tsx @@ -0,0 +1,186 @@ +import { EntityType, EntityTypeSource } from '@/types'; +import { Delete } from '@mui/icons-material'; +import { + Autocomplete, + Checkbox, + FormControl, + FormControlLabel, + Grid, + IconButton, + InputLabel, + MenuItem, + Select, + TextField, +} from '@mui/material'; +import { useMemo } from 'react'; + +export default function SourceEditor({ + entityTypes, + schema, + source, + sources, + isRoot, + onChange, + onRemove, +}: { + entityTypes: EntityType[]; + schema: Record; + source: EntityTypeSource; + sources: EntityTypeSource[]; + isRoot: boolean; + onChange: (newSource: EntityTypeSource) => void; + onRemove: () => void; +}) { + const dbSources = useMemo( + () => + Object.keys(schema) + .filter((k) => k.startsWith('TENANT_mod_fqm_manager.')) + .map((k) => k.substring(23)) + .filter((k) => !k.startsWith('query_results_')) + .toSorted(), + [schema], + ); + + return ( +
+ {source.alias} + + + + + Type + + labelId={`${source.alias}-source-type`} + fullWidth + value={source.type} + onChange={(e) => onChange({ alias: source.alias, type: e.target.value as 'db' | 'entity-type' })} + > + + db + + + entity-type + + + + + + onChange({ ...source, alias: e.target.value })} + inputProps={{ style: { fontFamily: 'monospace' } }} + /> + + + {source.type === 'entity-type' ? ( + + Entity type + + + ) : ( + ( + onChange({ ...source, target: e.target.value })} + /> + )} + /> + )} + + + + + + + + onChange({ ...source, useIdColumns: e.target.checked })} + /> + } + /> + + + {!isRoot && ( + <> + + + onChange({ + ...source, + join: { ...(source.join ?? {}), type: e.target.value } as EntityTypeSource['join'], + }) + } + inputProps={{ style: { fontFamily: 'monospace' } }} + /> + + + s.alias).filter((a) => a != source.alias)} + renderInput={(params) => ( + + onChange({ + ...source, + join: { ...(source.join ?? {}), joinTo: e.target.value } as EntityTypeSource['join'], + }) + } + /> + )} + /> + + + + onChange({ + ...source, + join: { ...(source.join ?? {}), condition: e.target.value } as EntityTypeSource['join'], + }) + } + inputProps={{ style: { fontFamily: 'monospace' } }} + /> + + + )} + +
+ ); +} diff --git a/entity-type-creator/pages/api/socket.ts b/entity-type-creator/pages/api/socket.ts index 22c38d81..23327215 100644 --- a/entity-type-creator/pages/api/socket.ts +++ b/entity-type-creator/pages/api/socket.ts @@ -112,6 +112,8 @@ export default function SocketHandler(req: NextApiRequest, res: NextApiResponse< findEntityTypes(); }); + socket.on('refresh-entity-types', () => findEntityTypes()); + socket.on('update-translations', async (newTranslations: Record) => { if (Object.keys(newTranslations).length === 0) return; diff --git a/entity-type-creator/types.ts b/entity-type-creator/types.ts index 36897b7a..e2724026 100644 --- a/entity-type-creator/types.ts +++ b/entity-type-creator/types.ts @@ -35,6 +35,7 @@ export interface DataType { export interface EntityTypeField { name: string; dataType: DataType; + sourceAlias?: string; isIdColumn?: boolean; idColumnName?: string; queryable?: boolean; @@ -53,6 +54,22 @@ export interface EntityTypeField { }; values?: { value: string; label: string }[]; } + +export interface EntityTypeSource { + type: 'db' | 'entity-type'; + target?: string; + alias: string; + id?: string; + join?: EntityTypeSourceJoin; + useIdColumns?: boolean; +} + +export interface EntityTypeSourceJoin { + type: string; + joinTo: string; + condition: string; +} + export interface EntityType { id: string; name: string; @@ -60,6 +77,7 @@ export interface EntityType { private?: boolean; customFieldEntityTypeId?: string; fromClause?: string; + sources?: EntityTypeSource[]; columns?: EntityTypeField[]; defaultSort?: { columnName: string; direction: string }[]; sourceView?: string; diff --git a/entity-type-creator/utils/formatter.ts b/entity-type-creator/utils/formatter.ts index 1f77833b..975d3835 100644 --- a/entity-type-creator/utils/formatter.ts +++ b/entity-type-creator/utils/formatter.ts @@ -24,6 +24,7 @@ const desiredRootKeyOrder = [ 'root', 'private', 'customFieldEntityTypeId', + 'sources', 'fromClause', 'columns', 'defaultSort', @@ -33,6 +34,7 @@ const desiredRootKeyOrder = [ const desiredDefaultSortKeyOrder = ['columnName', 'direction'] as (keyof Required['defaultSort'][0])[]; const desiredFieldKeyOrder = [ 'name', + 'sourceAlias', 'dataType', 'isIdColumn', 'idColumnName', @@ -63,7 +65,7 @@ function fixField(field: EntityTypeField) { export default function formatEntityType(data: EntityType) { data.columns = data.columns?.map(fixField); - data.fromClause = serializeSqlForTenantTemplating(data.fromClause); + data.fromClause = data.fromClause ? serializeSqlForTenantTemplating(data.fromClause) : undefined; if (data.defaultSort) { data.defaultSort = data.defaultSort.map((s) => preferredOrder(s, desiredDefaultSortKeyOrder)); }