mirror of
https://github.com/Canner/WrenAI.git
synced 2026-10-30 18:22:21 +08:00
Feature: onboarding UI changes (#16)
* feat(wren-ui): update the onboarding UI * feat(wren-ui): handle set relation logic * feat(wren-ui): add failed to connect alert block * chore(wren-ui): auto lint fix
This commit is contained in:
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 18 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 173 KiB |
@@ -1,40 +1,54 @@
|
||||
import { useEffect } from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { Modal, Form, Input, Select, Row, Col } from 'antd';
|
||||
import { Modal, Form, Select, Row, Col } from 'antd';
|
||||
import { ModalAction } from '@/hooks/useModalAction';
|
||||
import { ERROR_TEXTS } from '@/utils/error';
|
||||
import CombineFieldSelector from '@/components/selectors/CombineFieldSelector';
|
||||
import { JOIN_TYPE } from '@/utils/enum';
|
||||
import { RelationData, getJoinTypeText } from '@/utils/data';
|
||||
import { getJoinTypeText } from '@/utils/data';
|
||||
import useCombineFieldOptions from '@/hooks/useCombineFieldOptions';
|
||||
import { RelationsDataType } from '@/components/table/ModelRelationSelectionTable';
|
||||
import { SelectedRecommendRelations } from '@/components/pages/setup/DefineRelations';
|
||||
|
||||
export type RelationFieldValue = { [key: string]: any } & Pick<
|
||||
RelationData,
|
||||
'name' | 'joinType' | 'fromField' | 'toField' | 'properties'
|
||||
RelationsDataType,
|
||||
'name' | 'type' | 'fromField' | 'toField' | 'properties'
|
||||
>;
|
||||
|
||||
type Props = ModalAction<RelationFieldValue, RelationsDataType> & {
|
||||
model: string;
|
||||
loading?: boolean;
|
||||
allowSetDescription?: boolean;
|
||||
relations: SelectedRecommendRelations;
|
||||
};
|
||||
|
||||
export default function RelationModal(props: Props) {
|
||||
const {
|
||||
allowSetDescription = true,
|
||||
defaultValue,
|
||||
loading,
|
||||
model,
|
||||
onClose,
|
||||
onSubmit,
|
||||
relations,
|
||||
visible,
|
||||
} = props;
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const fromCombineField = useCombineFieldOptions({ model });
|
||||
|
||||
const toFieldModel = defaultValue?.toField.model;
|
||||
const toCombineField = useCombineFieldOptions({
|
||||
model: toFieldModel,
|
||||
excludeModels: [model],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
fromCombineField.onModelChange(model);
|
||||
form.setFieldsValue(defaultValue || {});
|
||||
|
||||
if (toFieldModel) {
|
||||
toCombineField.onModelChange(toFieldModel);
|
||||
}
|
||||
}, [form, defaultValue, visible]);
|
||||
|
||||
const relationTypeOptions = Object.keys(JOIN_TYPE).map((key) => ({
|
||||
@@ -42,11 +56,37 @@ export default function RelationModal(props: Props) {
|
||||
value: JOIN_TYPE[key],
|
||||
}));
|
||||
|
||||
const fromCombineField = useCombineFieldOptions({ model });
|
||||
const toCombineField = useCombineFieldOptions({
|
||||
model: defaultValue?.toField.model,
|
||||
excludeModels: [model],
|
||||
});
|
||||
const toCombineModelOptions = toCombineField.modelOptions.map(
|
||||
(modelOption) => {
|
||||
const modelList = Object.entries(relations).reduce(
|
||||
(acc, [modelName, modelRelations]) => {
|
||||
// For add relation, if the Model option has been selected in the From model relations, the Model option should be disabled
|
||||
if (modelName === model) {
|
||||
acc = [
|
||||
...acc,
|
||||
...modelRelations.map((relation) => relation.toField.model),
|
||||
];
|
||||
} else {
|
||||
const toFieldModelList = modelRelations.map(
|
||||
(relation) => relation.toField.model,
|
||||
);
|
||||
if (toFieldModelList.includes(model)) {
|
||||
acc = [...acc, modelName];
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const disabled = modelList.includes(modelOption.value);
|
||||
|
||||
return {
|
||||
...modelOption,
|
||||
disabled,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
const submit = () => {
|
||||
form
|
||||
@@ -70,25 +110,13 @@ export default function RelationModal(props: Props) {
|
||||
maskClosable={false}
|
||||
destroyOnClose
|
||||
afterClose={() => form.resetFields()}
|
||||
centered
|
||||
>
|
||||
<Form form={form} preserve={false} layout="vertical">
|
||||
<Form.Item
|
||||
label="Name"
|
||||
name="name"
|
||||
required
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: ERROR_TEXTS.ADD_RELATION.NAME.REQUIRED,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="From field"
|
||||
label="From"
|
||||
name="fromField"
|
||||
required
|
||||
rules={[
|
||||
@@ -109,7 +137,7 @@ export default function RelationModal(props: Props) {
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item
|
||||
label="To field"
|
||||
label="To"
|
||||
name="toField"
|
||||
required
|
||||
rules={[
|
||||
@@ -121,7 +149,7 @@ export default function RelationModal(props: Props) {
|
||||
>
|
||||
<CombineFieldSelector
|
||||
onModelChange={toCombineField.onModelChange}
|
||||
modelOptions={toCombineField.modelOptions}
|
||||
modelOptions={toCombineModelOptions}
|
||||
fieldOptions={toCombineField.fieldOptions}
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -129,7 +157,7 @@ export default function RelationModal(props: Props) {
|
||||
</Row>
|
||||
<Form.Item
|
||||
label="Relation type"
|
||||
name="joinType"
|
||||
name="type"
|
||||
required
|
||||
rules={[
|
||||
{
|
||||
@@ -143,11 +171,6 @@ export default function RelationModal(props: Props) {
|
||||
placeholder="Select a relation type"
|
||||
/>
|
||||
</Form.Item>
|
||||
{allowSetDescription && (
|
||||
<Form.Item label="Description" name={['properties', 'description']}>
|
||||
<Input.TextArea showCount maxLength={300} />
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import Image from 'next/image';
|
||||
import Link from 'next/link';
|
||||
import { Typography, Form, Row, Col, Button } from 'antd';
|
||||
import { Alert, Typography, Form, Row, Col, Button } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
import { DATA_SOURCES } from '@/utils/enum/dataSources';
|
||||
import { getDataSource } from './utils';
|
||||
@@ -19,10 +19,14 @@ interface Props {
|
||||
dataSource: DATA_SOURCES;
|
||||
onNext: (data: any) => void;
|
||||
onBack: () => void;
|
||||
connectInfo?: {
|
||||
connected: boolean;
|
||||
errorMessage?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default function ConnectDataSource(props: Props) {
|
||||
const { dataSource, onNext, onBack } = props;
|
||||
const { connectInfo, dataSource, onNext, onBack } = props;
|
||||
const [form] = Form.useForm();
|
||||
const current = getDataSource(dataSource);
|
||||
|
||||
@@ -43,18 +47,18 @@ export default function ConnectDataSource(props: Props) {
|
||||
Connect the data source
|
||||
</Typography.Title>
|
||||
<Typography.Text>
|
||||
We only support BigQuery for now. You can vote for your warehouse in our{' '}
|
||||
Vote for your preferred data source to be our next option on our{' '}
|
||||
<Link
|
||||
href="https://github.com/Canner/vulcan-sql/issues"
|
||||
href="https://github.com/Canner/WrenAI/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitHub issues
|
||||
GitHub
|
||||
</Link>
|
||||
!
|
||||
.
|
||||
</Typography.Text>
|
||||
|
||||
<StyledForm form={form} layout="vertical" className="p-6 mt-6">
|
||||
<StyledForm form={form} layout="vertical" className="p-6 my-6">
|
||||
<Row align="middle" className="mb-6">
|
||||
<Col span={12}>
|
||||
<DataSource className="d-inline-block px-4 py-2 bg-gray-2 gray-8">
|
||||
@@ -69,20 +73,38 @@ export default function ConnectDataSource(props: Props) {
|
||||
</DataSource>
|
||||
</Col>
|
||||
<Col className="text-right" span={12}>
|
||||
Learn {current.label} <Link href={current.guide}>setup guide</Link>.
|
||||
Learn more information in {current.label}{' '}
|
||||
<Link href={current.guide}>setup guide</Link>.
|
||||
</Col>
|
||||
</Row>
|
||||
<current.component />
|
||||
</StyledForm>
|
||||
|
||||
{connectInfo && !connectInfo.connected && (
|
||||
<Alert
|
||||
message="Failed to connect"
|
||||
description={
|
||||
connectInfo?.errorMessage || 'Cannot connect to data source'
|
||||
}
|
||||
type="error"
|
||||
showIcon
|
||||
className="my-6"
|
||||
/>
|
||||
)}
|
||||
|
||||
<Row gutter={16} className="pt-6">
|
||||
<Col span={12}>
|
||||
<Button onClick={onBack} size="large" block>
|
||||
<Button onClick={onBack} size="large" className="adm-onboarding-btn">
|
||||
Back
|
||||
</Button>
|
||||
</Col>
|
||||
<Col className="text-right" span={12}>
|
||||
<Button type="primary" size="large" block onClick={submit}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={submit}
|
||||
className="adm-onboarding-btn"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</Col>
|
||||
|
||||
@@ -20,8 +20,8 @@ export default function ContainerCard(props: Props) {
|
||||
<Card>
|
||||
<Steps current={step} className="mb-12">
|
||||
<Steps.Step title="Connect" />
|
||||
<Steps.Step title="Create models" />
|
||||
<Steps.Step title="Define relations" />
|
||||
<Steps.Step title="Select Tables" />
|
||||
<Steps.Step title="Define Relations" />
|
||||
</Steps>
|
||||
<div className="px-12 pb-6">{props.children}</div>
|
||||
</Card>
|
||||
|
||||
@@ -1,126 +0,0 @@
|
||||
import { useMemo } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { Button, Col, Form, Row, TableColumnsType, Typography } from 'antd';
|
||||
import { getColumnTypeIcon } from '@/utils/columnType';
|
||||
import { ERROR_TEXTS } from '@/utils/error';
|
||||
import { makeIterable, IterableComponent } from '@/utils/iteration';
|
||||
import ModelRelationSelectionTable, {
|
||||
SourceTable,
|
||||
SourceTableColumn,
|
||||
} from '@/components/table/ModelRelationSelectionTable';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
export interface SelectedSourceTables {
|
||||
[tableName: string]: SourceTableColumn[];
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onNext: (data: { models: SelectedSourceTables }) => void;
|
||||
onBack: () => void;
|
||||
selectedModels: string[];
|
||||
tables: SourceTable[];
|
||||
}
|
||||
|
||||
const columns: TableColumnsType<SourceTableColumn> = [
|
||||
{
|
||||
title: 'Field name',
|
||||
dataIndex: 'name',
|
||||
width: '65%',
|
||||
},
|
||||
{
|
||||
title: 'Type',
|
||||
dataIndex: 'type',
|
||||
width: '35%',
|
||||
render: (type) => (
|
||||
<div className="d-flex align-center">
|
||||
{getColumnTypeIcon({ type }, { className: 'mr-1' })}
|
||||
{type}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const SelectModelTemplate: IterableComponent = ({ index, name, fields }) => (
|
||||
<Form.Item
|
||||
className="mt-6"
|
||||
key={name}
|
||||
name={name}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: ERROR_TEXTS.SETUP_MODEL.FIELDS.REQUIRED,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<ModelRelationSelectionTable
|
||||
columns={columns}
|
||||
enableRowSelection
|
||||
dataSource={fields}
|
||||
tableTitle={name}
|
||||
rowKey={(record: SourceTableColumn) => `${name}-${record.name}-${index}`}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
|
||||
const SelectModelIterator = makeIterable(SelectModelTemplate);
|
||||
|
||||
export default function CreateModels(props: Props) {
|
||||
const { onBack, onNext, selectedModels, tables } = props;
|
||||
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const selectModelFields = useMemo(() => {
|
||||
return selectedModels.map((modelName) => {
|
||||
const table = tables.find((table) => table.displayName === modelName);
|
||||
return {
|
||||
name: modelName,
|
||||
fields: table.columns,
|
||||
};
|
||||
});
|
||||
}, [selectedModels, tables]);
|
||||
|
||||
const submit = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
onNext && onNext({ models: { ...values } });
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title level={1} className="mb-3">
|
||||
Create data model from tables
|
||||
</Title>
|
||||
<Text>
|
||||
Data models are created directly from your data source tables. It act as
|
||||
a “view” of your underlying table to transform and extend on the
|
||||
original data.{` `}
|
||||
<Link href="" target="_blank" rel="noopener noreferrer">
|
||||
Learn more
|
||||
</Link>
|
||||
</Text>
|
||||
|
||||
<Form form={form} layout="vertical" className="my-6">
|
||||
<SelectModelIterator data={selectModelFields} />
|
||||
</Form>
|
||||
|
||||
<Row gutter={16} className="pt-6">
|
||||
<Col span={12}>
|
||||
<Button onClick={onBack} size="large" block>
|
||||
Back
|
||||
</Button>
|
||||
</Col>
|
||||
<Col className="text-right" span={12}>
|
||||
<Button type="primary" size="large" block onClick={submit}>
|
||||
Next
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import { omit } from 'lodash';
|
||||
import { Button, Col, Popconfirm, Row, Space, Typography } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
@@ -9,9 +8,9 @@ import PlusOutlined from '@ant-design/icons/PlusOutlined';
|
||||
import ModelRelationSelectionTable, {
|
||||
RelationsDataType,
|
||||
} from '@/components/table/ModelRelationSelectionTable';
|
||||
import { columns as defaultColumns } from '@/components/pages/setup/RecommendRelations';
|
||||
import { getJoinTypeText } from '@/utils/data';
|
||||
import useModalAction from '@/hooks/useModalAction';
|
||||
import AddRelationModal from '@/components/modals/AddRelationModal';
|
||||
import RelationModal from '@/components/modals/AddRelationModal';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
@@ -20,9 +19,10 @@ export interface SelectedRecommendRelations {
|
||||
}
|
||||
|
||||
interface Props {
|
||||
selectedRecommendRelations: SelectedRecommendRelations;
|
||||
recommendRelations: SelectedRecommendRelations;
|
||||
onNext: (data: { relations: SelectedRecommendRelations }) => void;
|
||||
onBack: () => void;
|
||||
onSkip: () => void;
|
||||
}
|
||||
|
||||
interface EditableRelationTableProps {
|
||||
@@ -40,7 +40,36 @@ function EditableRelationTable(props: EditableRelationTableProps) {
|
||||
const { index, modelName, onSetRelation, onDeleteRow, relations } = props;
|
||||
|
||||
const columns: ColumnsType<RelationsDataType> = [
|
||||
...defaultColumns,
|
||||
{
|
||||
title: 'From',
|
||||
dataIndex: 'fromField',
|
||||
key: 'fromField',
|
||||
render: (fromField) => `${fromField.model}.${fromField.field}`,
|
||||
width: '35%',
|
||||
},
|
||||
{
|
||||
title: 'To',
|
||||
dataIndex: 'toField',
|
||||
key: 'toField',
|
||||
render: (toField) => `${toField.model}.${toField.field}`,
|
||||
width: '35%',
|
||||
},
|
||||
{
|
||||
title: 'Relation type',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
render: (type, relation) => (
|
||||
<>
|
||||
{getJoinTypeText(type)}
|
||||
{relation.isAutoGenerated && (
|
||||
<Text className="pl-1" type="secondary">
|
||||
(auto-generated)
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
width: '30%',
|
||||
},
|
||||
{
|
||||
title: '',
|
||||
key: 'action',
|
||||
@@ -85,7 +114,7 @@ function EditableRelationTable(props: EditableRelationTableProps) {
|
||||
title="Add relation"
|
||||
>
|
||||
<PlusOutlined />
|
||||
Add relation
|
||||
Add
|
||||
</Button>
|
||||
)}
|
||||
rowKey={(record: RelationsDataType) =>
|
||||
@@ -97,30 +126,27 @@ function EditableRelationTable(props: EditableRelationTableProps) {
|
||||
}
|
||||
|
||||
export default function DefineRelations(props: Props) {
|
||||
const { selectedRecommendRelations, onBack, onNext } = props;
|
||||
const { recommendRelations, onBack, onNext, onSkip } = props;
|
||||
|
||||
const [relations, setRelations] = useState<SelectedRecommendRelations>(
|
||||
selectedRecommendRelations,
|
||||
);
|
||||
const [relations, setRelations] =
|
||||
useState<SelectedRecommendRelations>(recommendRelations);
|
||||
|
||||
const [selectedRelation, setSelectedRelation] = useState<{
|
||||
modelName: string;
|
||||
defaultValue?: RelationsDataType;
|
||||
}>(null);
|
||||
|
||||
const addRelationModal = useModalAction();
|
||||
const relationModal = useModalAction();
|
||||
|
||||
// check is the relation is auto-generated or not
|
||||
const isRecommendRelation = (
|
||||
modelName: string,
|
||||
relation: RelationsDataType,
|
||||
) => {
|
||||
const isOriginalRelation = (
|
||||
selectedRecommendRelations[modelName] || []
|
||||
).find(
|
||||
const isOriginalRelation = (recommendRelations[modelName] || []).find(
|
||||
(originalRelation) =>
|
||||
JSON.stringify(omit(originalRelation, ['isAutoGenerated'])) ===
|
||||
JSON.stringify(relation),
|
||||
JSON.stringify(omit(originalRelation, ['name', 'isAutoGenerated'])) ===
|
||||
JSON.stringify(omit(relation, ['name', 'isAutoGenerated'])),
|
||||
);
|
||||
return isOriginalRelation?.isAutoGenerated || false;
|
||||
};
|
||||
@@ -139,7 +165,7 @@ export default function DefineRelations(props: Props) {
|
||||
|
||||
const onCloseModal = () => {
|
||||
setSelectedRelation(null);
|
||||
addRelationModal.closeModal();
|
||||
relationModal.closeModal();
|
||||
};
|
||||
|
||||
const onDeleteRow = (
|
||||
@@ -161,7 +187,7 @@ export default function DefineRelations(props: Props) {
|
||||
defaultValue?: RelationsDataType;
|
||||
}) => {
|
||||
setSelectedRelation(payload);
|
||||
addRelationModal.openModal();
|
||||
relationModal.openModal();
|
||||
};
|
||||
|
||||
const onUpdateRelation = (
|
||||
@@ -192,16 +218,13 @@ export default function DefineRelations(props: Props) {
|
||||
return (
|
||||
<div>
|
||||
<Title level={1} className="mb-3">
|
||||
Define the relations
|
||||
Define relations
|
||||
</Title>
|
||||
<Text>
|
||||
After creating your data models in step 2, you can specify how they
|
||||
should be joined together by defining the relationships. We will
|
||||
automatically create the relations based on the 'primary' and 'foreign'
|
||||
in your data sources.{` `}
|
||||
<Link href="" target="_blank" rel="noopener noreferrer">
|
||||
Learn more
|
||||
</Link>
|
||||
You can define the relations among selected tables. We will
|
||||
automatically list suggested relations based on the primary and foreign
|
||||
key you have already defined in your data source. Relations will be
|
||||
added into data models.
|
||||
</Text>
|
||||
<div className="my-6">
|
||||
{Object.entries(relations).map(([modelName, relations = []], index) => (
|
||||
@@ -217,19 +240,32 @@ export default function DefineRelations(props: Props) {
|
||||
</div>
|
||||
<Row gutter={16} className="pt-6">
|
||||
<Col span={12}>
|
||||
<Button onClick={onBack} size="large" block>
|
||||
<Button onClick={onBack} size="large" className="adm-onboarding-btn">
|
||||
Back
|
||||
</Button>
|
||||
</Col>
|
||||
<Col className="text-right" span={12}>
|
||||
<Button type="primary" size="large" block onClick={submit}>
|
||||
Next
|
||||
<Button
|
||||
className="mr-4 gray-7 adm-onboarding-btn"
|
||||
type="text"
|
||||
size="large"
|
||||
onClick={onSkip}
|
||||
>
|
||||
Skip this step
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={submit}
|
||||
className="adm-onboarding-btn"
|
||||
>
|
||||
Finish
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<AddRelationModal
|
||||
<RelationModal
|
||||
{...relationModal.state}
|
||||
model={selectedRelation?.modelName}
|
||||
{...addRelationModal.state}
|
||||
onSubmit={async (values) => {
|
||||
if (selectedRelation?.defaultValue) {
|
||||
onUpdateRelation(
|
||||
@@ -243,12 +279,12 @@ export default function DefineRelations(props: Props) {
|
||||
setSelectedRelation(null);
|
||||
}}
|
||||
onClose={onCloseModal}
|
||||
allowSetDescription={false}
|
||||
defaultValue={
|
||||
selectedRelation?.defaultValue
|
||||
? omit(selectedRelation.defaultValue, ['isAutoGenerated'])
|
||||
: undefined
|
||||
}
|
||||
relations={relations}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,128 +0,0 @@
|
||||
import Link from 'next/link';
|
||||
import { Button, Col, Form, Row, Typography } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import ModelRelationSelectionTable, {
|
||||
RelationsDataType,
|
||||
} from '@/components/table/ModelRelationSelectionTable';
|
||||
import { SelectedRecommendRelations } from '@/components/pages/setup/DefineRelations';
|
||||
import { makeIterable, IterableComponent } from '@/utils/iteration';
|
||||
import { getJoinTypeText } from '@/utils/data';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
interface Props {
|
||||
// TODO: update type when connecting to backend
|
||||
recommendRelations: Array<{
|
||||
name: string;
|
||||
relations: RelationsDataType[];
|
||||
}>;
|
||||
onNext: (data: SelectedRecommendRelations) => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
export const columns: ColumnsType<RelationsDataType> = [
|
||||
{
|
||||
title: 'Name',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: '25%',
|
||||
},
|
||||
{
|
||||
title: 'From field',
|
||||
dataIndex: 'fromField',
|
||||
key: 'fromField',
|
||||
render: (fromField) => `${fromField.model}.${fromField.field}`,
|
||||
width: '25%',
|
||||
},
|
||||
{
|
||||
title: 'To field',
|
||||
dataIndex: 'toField',
|
||||
key: 'toField',
|
||||
render: (toField) => `${toField.model}.${toField.field}`,
|
||||
width: '25%',
|
||||
},
|
||||
{
|
||||
title: 'Relation type',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
render: (type, relation) => (
|
||||
<>
|
||||
{getJoinTypeText(type)}
|
||||
{relation.isAutoGenerated && (
|
||||
<Text className="pl-1" type="secondary">
|
||||
(auto-generated)
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
),
|
||||
width: '25%',
|
||||
},
|
||||
];
|
||||
|
||||
const SelectRelationTemplate: IterableComponent = ({
|
||||
name,
|
||||
index,
|
||||
relations,
|
||||
}) => (
|
||||
<Form.Item key={name} className="mt-6" name={name}>
|
||||
<ModelRelationSelectionTable
|
||||
columns={columns}
|
||||
enableRowSelection
|
||||
dataSource={relations}
|
||||
tableTitle={name}
|
||||
rowKey={(record: RelationsDataType) => `${name}-${record.name}-${index}`}
|
||||
/>
|
||||
</Form.Item>
|
||||
);
|
||||
|
||||
const SelectRelationIterator = makeIterable(SelectRelationTemplate);
|
||||
|
||||
export default function RecommendRelations(props: Props) {
|
||||
const { recommendRelations, onBack, onNext } = props;
|
||||
const [form] = Form.useForm();
|
||||
|
||||
const submit = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
onNext && onNext({ selectedRecommendRelations: values });
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Title level={1} className="mb-3">
|
||||
Define the relations
|
||||
</Title>
|
||||
<Text>
|
||||
After creating your data models in step 2, you can specify how they
|
||||
should be joined together by defining the relationships. We will
|
||||
automatically create the relations based on the 'primary' and 'foreign'
|
||||
in your data sources.{` `}
|
||||
<Link href="" target="_blank" rel="noopener noreferrer">
|
||||
Learn more
|
||||
</Link>
|
||||
</Text>
|
||||
|
||||
<Form form={form} layout="vertical" className="my-6">
|
||||
<SelectRelationIterator data={recommendRelations} />
|
||||
</Form>
|
||||
|
||||
<Row gutter={16} className="pt-6">
|
||||
<Col span={12}>
|
||||
<Button onClick={onBack} size="large" block>
|
||||
Back
|
||||
</Button>
|
||||
</Col>
|
||||
<Col className="text-right" span={12}>
|
||||
<Button type="primary" size="large" block onClick={submit}>
|
||||
Next
|
||||
</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -3,20 +3,23 @@ import { Button, Col, Form, Row, Typography } from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { ERROR_TEXTS } from '@/utils/error';
|
||||
import MultiSelectBox from '@/components/table/MultiSelectBox';
|
||||
import { SourceTable } from '@/components/table/ModelRelationSelectionTable';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
export interface SourceTable {
|
||||
name: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
tables: SourceTable[];
|
||||
onNext: (data: { selectedModels: string[] }) => void;
|
||||
onNext: (data: { selectedTables: string[] }) => void;
|
||||
onBack: () => void;
|
||||
}
|
||||
|
||||
const columns: ColumnsType<SourceTable> = [
|
||||
{
|
||||
title: 'Table name',
|
||||
dataIndex: 'displayName',
|
||||
dataIndex: 'name',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -26,14 +29,14 @@ export default function SelectModels(props: Props) {
|
||||
|
||||
const items = tables.map((item) => ({
|
||||
...item,
|
||||
value: item.displayName,
|
||||
value: item.name,
|
||||
}));
|
||||
|
||||
const submit = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
onNext && onNext({ selectedModels: values.models });
|
||||
onNext && onNext({ selectedTables: values.tables });
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
@@ -43,20 +46,21 @@ export default function SelectModels(props: Props) {
|
||||
return (
|
||||
<div>
|
||||
<Title level={1} className="mb-3">
|
||||
Create data model from tables
|
||||
Select tables to create data models
|
||||
</Title>
|
||||
<Text>
|
||||
Data models are created directly from your data source tables. It act as
|
||||
a “view” of your underlying table to transform and extend on the
|
||||
original data.{` `}
|
||||
We will create data models from selected tables. It will help AI to
|
||||
better know your data.
|
||||
<br />
|
||||
<Link href="" target="_blank" rel="noopener noreferrer">
|
||||
Learn more
|
||||
</Link>
|
||||
</Link>{' '}
|
||||
about data models.
|
||||
</Text>
|
||||
<div className="my-6">
|
||||
<Form form={form} layout="vertical" style={{ marginTop: 8 }}>
|
||||
<Form.Item
|
||||
name="models"
|
||||
name="tables"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
@@ -70,12 +74,17 @@ export default function SelectModels(props: Props) {
|
||||
</div>
|
||||
<Row gutter={16} className="pt-6">
|
||||
<Col span={12}>
|
||||
<Button onClick={onBack} size="large" block>
|
||||
<Button onClick={onBack} size="large" className="adm-onboarding-btn">
|
||||
Back
|
||||
</Button>
|
||||
</Col>
|
||||
<Col className="text-right" span={12}>
|
||||
<Button type="primary" size="large" block onClick={submit}>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
onClick={submit}
|
||||
className="adm-onboarding-btn"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</Col>
|
||||
|
||||
@@ -87,29 +87,29 @@ export default function Starter(props) {
|
||||
return (
|
||||
<>
|
||||
<Typography.Title level={1} className="mb-3">
|
||||
Connect the data source
|
||||
Connect a data source
|
||||
</Typography.Title>
|
||||
<Typography.Text>
|
||||
We only support BigQuery for now. You can vote for your warehouse in our{' '}
|
||||
Vote for your preferred data source to be our next option on our{' '}
|
||||
<Link
|
||||
href="https://github.com/Canner/vulcan-sql/issues"
|
||||
href="https://github.com/Canner/WrenAI/issues"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
GitHub issues
|
||||
GitHub
|
||||
</Link>
|
||||
!
|
||||
.
|
||||
</Typography.Text>
|
||||
<Row className="mt-6" gutter={16}>
|
||||
<Row className="mt-6" gutter={[16, 16]}>
|
||||
<DataSourceIterator data={dataSources} onSelect={onSelectDataSource} />
|
||||
</Row>
|
||||
|
||||
<div className="py-8" />
|
||||
|
||||
<Typography.Title level={1} className="mb-3">
|
||||
or play around with a template
|
||||
Play around with sample data
|
||||
</Typography.Title>
|
||||
<Row className="mt-6" gutter={16}>
|
||||
<Row className="mt-6" gutter={[16, 16]}>
|
||||
<TemplatesIterator data={templates} onSelect={onSelectTemplate} />
|
||||
</Row>
|
||||
|
||||
|
||||
@@ -59,7 +59,6 @@ export default function BigQueryProperties() {
|
||||
label="Display name"
|
||||
required
|
||||
name="displayName"
|
||||
extra="The name will be called in VulcanSQL."
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
@@ -83,30 +82,17 @@ export default function BigQueryProperties() {
|
||||
<Input placeholder="The GCP project ID" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Dataset name"
|
||||
label="Dataset ID"
|
||||
required
|
||||
name="dataset"
|
||||
name="datasetId"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: ERROR_TEXTS.CONNECTION.DATASET.REQUIRED,
|
||||
message: ERROR_TEXTS.CONNECTION.DATASET_ID.REQUIRED,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="The dataset name" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Data Location"
|
||||
required
|
||||
name="location"
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: ERROR_TEXTS.CONNECTION.LOCATION.REQUIRED,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<Input placeholder="The location of the dataset. e.g. US." />
|
||||
<Input />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label="Credentials"
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import Starter from './Starter';
|
||||
import ConnectDataSource from './ConnectDataSource';
|
||||
import SelectModels from './SelectModels';
|
||||
import CreateModels from './CreateModels';
|
||||
import RecommendRelations from './RecommendRelations';
|
||||
import DefineRelations from './DefineRelations';
|
||||
import { SETUP, DATA_SOURCES, DEMO_TEMPLATES } from '@/utils/enum';
|
||||
import BigQueryProperties from './dataSources/BigQueryProperties';
|
||||
@@ -14,8 +12,6 @@ type SetupStep = {
|
||||
props?: React.ComponentProps<typeof Starter> &
|
||||
React.ComponentProps<typeof ConnectDataSource> &
|
||||
React.ComponentProps<typeof SelectModels> &
|
||||
React.ComponentProps<typeof CreateModels> &
|
||||
React.ComponentProps<typeof RecommendRelations> &
|
||||
React.ComponentProps<typeof DefineRelations>,
|
||||
) => JSX.Element;
|
||||
maxWidth?: number;
|
||||
@@ -36,19 +32,12 @@ export const SETUP_STEPS = {
|
||||
[SETUP.CREATE_DATA_SOURCE]: {
|
||||
step: 0,
|
||||
component: ConnectDataSource,
|
||||
maxWidth: 800,
|
||||
maxWidth: 960,
|
||||
},
|
||||
[SETUP.SELECT_MODELS]: {
|
||||
step: 1,
|
||||
component: SelectModels,
|
||||
},
|
||||
[SETUP.CREATE_MODELS]: {
|
||||
step: 1,
|
||||
component: CreateModels,
|
||||
},
|
||||
[SETUP.RECOMMEND_RELATIONS]: {
|
||||
step: 2,
|
||||
component: RecommendRelations,
|
||||
maxWidth: 960,
|
||||
},
|
||||
[SETUP.DEFINE_RELATIONS]: {
|
||||
step: 2,
|
||||
@@ -63,21 +52,15 @@ export const DATA_SOURCE_OPTIONS = {
|
||||
guide: '',
|
||||
disabled: false,
|
||||
},
|
||||
[DATA_SOURCES.DATA_BRICKS]: {
|
||||
label: 'Databricks',
|
||||
logo: '/images/dataSource/dataBricks.svg',
|
||||
[DATA_SOURCES.DUCKDB]: {
|
||||
label: 'DuckDB',
|
||||
logo: '/images/dataSource/duckDb.svg',
|
||||
guide: '',
|
||||
disabled: true,
|
||||
},
|
||||
[DATA_SOURCES.SNOWFLAKE]: {
|
||||
label: 'Snowflake',
|
||||
logo: '/images/dataSource/snowflake.svg',
|
||||
guide: '',
|
||||
disabled: true,
|
||||
},
|
||||
[DATA_SOURCES.TRINO]: {
|
||||
label: 'Trino',
|
||||
logo: '',
|
||||
[DATA_SOURCES.PG_SQL]: {
|
||||
label: 'PostgreSQL',
|
||||
logo: '/images/dataSource/postgreSql.svg',
|
||||
guide: '',
|
||||
disabled: true,
|
||||
},
|
||||
|
||||
@@ -8,30 +8,18 @@ interface ModelField {
|
||||
field: string;
|
||||
}
|
||||
|
||||
export interface SourceTableColumn {
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface SourceTable {
|
||||
id: string;
|
||||
sqlName: string;
|
||||
displayName: string;
|
||||
columns: SourceTableColumn[];
|
||||
}
|
||||
|
||||
export interface RelationsDataType {
|
||||
name: string;
|
||||
fromField: ModelField;
|
||||
isAutoGenerated: boolean;
|
||||
joinType: JOIN_TYPE;
|
||||
type: JOIN_TYPE;
|
||||
toField: ModelField;
|
||||
properties: Record<string, any>;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
columns: ColumnsType<RelationsDataType> | ColumnsType<SourceTableColumn>;
|
||||
dataSource: RelationsDataType[] | SourceTableColumn[];
|
||||
columns: ColumnsType<RelationsDataType>;
|
||||
dataSource: RelationsDataType[];
|
||||
enableRowSelection?: boolean;
|
||||
extra?: (
|
||||
onCollapseOpen: (
|
||||
@@ -41,7 +29,7 @@ interface Props {
|
||||
) => React.ReactNode;
|
||||
onChange?: (value: any | null) => void;
|
||||
tableTitle: string;
|
||||
rowKey: (record: RelationsDataType | SourceTableColumn) => string;
|
||||
rowKey: (record: RelationsDataType) => string;
|
||||
}
|
||||
|
||||
export default function ModelRelationSelectionTable(props: Props) {
|
||||
|
||||
@@ -19,6 +19,12 @@ export default function useCombineFieldOptions(props: Props) {
|
||||
name: 'orders',
|
||||
properties: { type: 'Orders' },
|
||||
},
|
||||
{
|
||||
name: 'custkey',
|
||||
},
|
||||
{
|
||||
name: 'id',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -28,6 +34,9 @@ export default function useCombineFieldOptions(props: Props) {
|
||||
name: 'lineitem',
|
||||
properties: { type: 'Lineitem' },
|
||||
},
|
||||
{
|
||||
name: 'orderkey',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -41,6 +50,23 @@ export default function useCombineFieldOptions(props: Props) {
|
||||
name: 'discount',
|
||||
properties: { type: 'REAL' },
|
||||
},
|
||||
{
|
||||
name: 'orderkey',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'trans',
|
||||
columns: [
|
||||
{
|
||||
name: 'custkey',
|
||||
},
|
||||
{
|
||||
name: 'col_a',
|
||||
},
|
||||
{
|
||||
name: 'col_b',
|
||||
},
|
||||
],
|
||||
},
|
||||
].filter((item) => !(excludeModels && excludeModels.includes(item.name)));
|
||||
|
||||
@@ -1,46 +1,29 @@
|
||||
import { useState } from 'react';
|
||||
import { SETUP } from '@/utils/enum';
|
||||
import { useRouter } from 'next/router';
|
||||
import { SelectedSourceTables } from '@/components/pages/setup/CreateModels';
|
||||
import { SourceTable } from '@/components/pages/setup/SelectModels';
|
||||
|
||||
export default function useSetupModels() {
|
||||
const [stepKey, setStepKey] = useState(SETUP.SELECT_MODELS);
|
||||
const [selectedModels, setSelectedModels] = useState<any | undefined>(
|
||||
undefined,
|
||||
);
|
||||
const [stepKey] = useState(SETUP.SELECT_MODELS);
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const submitModels = async (_models: SelectedSourceTables) => {
|
||||
const submitModels = async (_tables: SourceTable[]) => {
|
||||
// TODO: implement submitModels API
|
||||
router.push('/setup/relations');
|
||||
};
|
||||
|
||||
const onBack = () => {
|
||||
if (stepKey === SETUP.CREATE_MODELS) {
|
||||
setStepKey(SETUP.SELECT_MODELS);
|
||||
} else {
|
||||
router.push('/setup/connection');
|
||||
}
|
||||
router.push('/setup/connection');
|
||||
};
|
||||
|
||||
const onNext = (data?: {
|
||||
selectedModels: string[];
|
||||
models: SelectedSourceTables;
|
||||
}) => {
|
||||
if (stepKey === SETUP.SELECT_MODELS) {
|
||||
setSelectedModels(data.selectedModels);
|
||||
setStepKey(SETUP.CREATE_MODELS);
|
||||
}
|
||||
|
||||
if (stepKey === SETUP.CREATE_MODELS) {
|
||||
submitModels(data.models);
|
||||
}
|
||||
const onNext = (data?: { selectedTables: string[] }) => {
|
||||
const tables = data.selectedTables.map((table) => ({ name: table }));
|
||||
submitModels(tables);
|
||||
};
|
||||
|
||||
return {
|
||||
stepKey,
|
||||
selectedModels,
|
||||
tables,
|
||||
onBack,
|
||||
onNext,
|
||||
@@ -50,163 +33,12 @@ export default function useSetupModels() {
|
||||
// TODO: remove it when connecting to backend
|
||||
const tables = [
|
||||
{
|
||||
id: 'a3e9aba0-c1a7-43bb-8bae-da65256ec5a3',
|
||||
sqlName: 'customer',
|
||||
displayName: 'customer',
|
||||
columns: [
|
||||
{
|
||||
name: 'address',
|
||||
type: 'VARCHAR',
|
||||
},
|
||||
{
|
||||
name: 'custkey',
|
||||
type: 'BIGINT',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'VARCHAR',
|
||||
},
|
||||
{
|
||||
name: 'nationkey',
|
||||
type: 'BIGINT',
|
||||
},
|
||||
],
|
||||
name: 'orders',
|
||||
},
|
||||
{
|
||||
id: '3f74ab82-aa22-476c-9577-273db3d1f75c',
|
||||
sqlName: 'lineitem',
|
||||
displayName: 'lineitem',
|
||||
columns: [
|
||||
{
|
||||
name: 'comment',
|
||||
type: 'VARCHAR',
|
||||
},
|
||||
{
|
||||
name: 'commitdate',
|
||||
type: 'DATE',
|
||||
},
|
||||
{
|
||||
name: 'discount',
|
||||
type: 'DOUBLE',
|
||||
},
|
||||
{
|
||||
name: 'extendedprice',
|
||||
type: 'DOUBLE',
|
||||
},
|
||||
{
|
||||
name: 'linenumber',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'linestatus',
|
||||
type: 'VARCHAR',
|
||||
},
|
||||
{
|
||||
name: 'orderkey',
|
||||
type: 'BIGINT',
|
||||
},
|
||||
{
|
||||
name: 'partkey',
|
||||
type: 'BIGINT',
|
||||
},
|
||||
{
|
||||
name: 'quantity',
|
||||
type: 'DOUBLE',
|
||||
},
|
||||
{
|
||||
name: 'receiptdate',
|
||||
type: 'DATE',
|
||||
},
|
||||
{
|
||||
name: 'returnflag',
|
||||
type: 'VARCHAR',
|
||||
},
|
||||
{
|
||||
name: 'shipdate',
|
||||
type: 'DATE',
|
||||
},
|
||||
{
|
||||
name: 'shipinstruct',
|
||||
type: 'VARCHAR',
|
||||
},
|
||||
{
|
||||
name: 'shipmode',
|
||||
type: 'VARCHAR',
|
||||
},
|
||||
{
|
||||
name: 'suppkey',
|
||||
type: 'BIGINT',
|
||||
},
|
||||
{
|
||||
name: 'tax',
|
||||
type: 'DOUBLE',
|
||||
},
|
||||
],
|
||||
name: 'customers',
|
||||
},
|
||||
{
|
||||
id: 'a6339b68-0ffb-4268-8cfd-68b8206a852f',
|
||||
sqlName: 'nation',
|
||||
displayName: 'nation',
|
||||
columns: [
|
||||
{
|
||||
name: 'comment',
|
||||
type: 'VARCHAR',
|
||||
},
|
||||
{
|
||||
name: 'name',
|
||||
type: 'VARCHAR',
|
||||
},
|
||||
{
|
||||
name: 'nationkey',
|
||||
type: 'BIGINT',
|
||||
},
|
||||
{
|
||||
name: 'regionkey',
|
||||
type: 'BIGINT',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: '1e6b4ca6-c1ba-43de-ad5c-dcd203e5fed2',
|
||||
sqlName: 'orders',
|
||||
displayName: 'orders',
|
||||
columns: [
|
||||
{
|
||||
name: 'clerk',
|
||||
type: 'VARCHAR',
|
||||
},
|
||||
{
|
||||
name: 'comment',
|
||||
type: 'VARCHAR',
|
||||
},
|
||||
{
|
||||
name: 'custkey',
|
||||
type: 'BIGINT',
|
||||
},
|
||||
{
|
||||
name: 'orderdate',
|
||||
type: 'DATE',
|
||||
},
|
||||
{
|
||||
name: 'orderkey',
|
||||
type: 'BIGINT',
|
||||
},
|
||||
{
|
||||
name: 'orderpriority',
|
||||
type: 'VARCHAR',
|
||||
},
|
||||
{
|
||||
name: 'orderstatus',
|
||||
type: 'VARCHAR',
|
||||
},
|
||||
{
|
||||
name: 'shippriority',
|
||||
type: 'INTEGER',
|
||||
},
|
||||
{
|
||||
name: 'totalprice',
|
||||
type: 'DOUBLE',
|
||||
},
|
||||
],
|
||||
name: 'products',
|
||||
},
|
||||
];
|
||||
|
||||
@@ -5,51 +5,44 @@ import { SelectedRecommendRelations } from '@/components/pages/setup/DefineRelat
|
||||
import { Path } from '@/utils/enum';
|
||||
|
||||
export default function useSetupRelations() {
|
||||
const [stepKey, setStepKey] = useState(SETUP.RECOMMEND_RELATIONS);
|
||||
const [selectedRecommendRelations, setSelectedRecommendRelations] = useState<
|
||||
SelectedRecommendRelations | undefined
|
||||
>(undefined);
|
||||
|
||||
const [stepKey] = useState(SETUP.DEFINE_RELATIONS);
|
||||
const router = useRouter();
|
||||
|
||||
const submitReleations = async (_relations: SelectedRecommendRelations) => {
|
||||
// TODO: implement submitReleations API
|
||||
router.push(Path.Exploration);
|
||||
router.push(Path.HOME);
|
||||
};
|
||||
|
||||
const onBack = () => {
|
||||
if (stepKey === SETUP.DEFINE_RELATIONS) {
|
||||
setStepKey(SETUP.RECOMMEND_RELATIONS);
|
||||
} else {
|
||||
router.push('/setup/models');
|
||||
}
|
||||
router.push('/setup/models');
|
||||
};
|
||||
|
||||
const onNext = (data: {
|
||||
selectedRecommendRelations: SelectedRecommendRelations;
|
||||
relations: SelectedRecommendRelations;
|
||||
}) => {
|
||||
if (stepKey === SETUP.RECOMMEND_RELATIONS) {
|
||||
setSelectedRecommendRelations(data.selectedRecommendRelations);
|
||||
setStepKey(SETUP.DEFINE_RELATIONS);
|
||||
}
|
||||
|
||||
if (stepKey === SETUP.DEFINE_RELATIONS) {
|
||||
submitReleations(data.relations);
|
||||
}
|
||||
const onNext = (data: { relations: SelectedRecommendRelations }) => {
|
||||
submitReleations(data.relations);
|
||||
};
|
||||
|
||||
const onSkip = () => router.push(Path.HOME);
|
||||
|
||||
const recommendRelations = recommendRelationsData.reduce(
|
||||
(acc, currentValue) => {
|
||||
const { name, relations } = currentValue;
|
||||
acc[name] = relations;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
return {
|
||||
stepKey,
|
||||
recommendRelations,
|
||||
selectedRecommendRelations,
|
||||
onBack,
|
||||
onNext,
|
||||
onSkip,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO: remove it when connecting to backend
|
||||
const recommendRelations = [
|
||||
const recommendRelationsData = [
|
||||
{
|
||||
name: 'Customer',
|
||||
relations: [
|
||||
@@ -70,15 +63,19 @@ const recommendRelations = [
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Order',
|
||||
name: 'Orders',
|
||||
relations: [
|
||||
{
|
||||
name: 'Order_Lineitem',
|
||||
fromField: { model: 'Order', field: 'orderkey' },
|
||||
name: 'Orders_Lineitem',
|
||||
fromField: { model: 'Orders', field: 'orderkey' },
|
||||
toField: { model: 'Lineitem', field: 'orderkey' },
|
||||
type: JOIN_TYPE.ONE_TO_MANY,
|
||||
isAutoGenerated: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'trans',
|
||||
relations: [],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -5,19 +5,14 @@ import useSetupModels from '@/hooks/useSetupModels';
|
||||
import { SETUP_STEPS } from '@/components/pages/setup/utils';
|
||||
|
||||
export default function SetupModels() {
|
||||
const { stepKey, selectedModels, tables, onNext, onBack } = useSetupModels();
|
||||
const { stepKey, tables, onNext, onBack } = useSetupModels();
|
||||
|
||||
const current = useMemo(() => SETUP_STEPS[stepKey], [stepKey]);
|
||||
|
||||
return (
|
||||
<SimpleLayout>
|
||||
<ContainerCard step={current.step} maxWidth={current.maxWidth}>
|
||||
<current.component
|
||||
selectedModels={selectedModels}
|
||||
tables={tables}
|
||||
onNext={onNext}
|
||||
onBack={onBack}
|
||||
/>
|
||||
<current.component tables={tables} onNext={onNext} onBack={onBack} />
|
||||
</ContainerCard>
|
||||
</SimpleLayout>
|
||||
);
|
||||
|
||||
@@ -5,13 +5,8 @@ import useSetupRelations from '@/hooks/useSetupRelations';
|
||||
import { SETUP_STEPS } from '@/components/pages/setup/utils';
|
||||
|
||||
export default function SetupRelations() {
|
||||
const {
|
||||
stepKey,
|
||||
recommendRelations,
|
||||
selectedRecommendRelations,
|
||||
onNext,
|
||||
onBack,
|
||||
} = useSetupRelations();
|
||||
const { stepKey, recommendRelations, onNext, onBack, onSkip } =
|
||||
useSetupRelations();
|
||||
|
||||
const current = useMemo(() => SETUP_STEPS[stepKey], [stepKey]);
|
||||
|
||||
@@ -20,9 +15,9 @@ export default function SetupRelations() {
|
||||
<ContainerCard step={current.step} maxWidth={current.maxWidth}>
|
||||
<current.component
|
||||
recommendRelations={recommendRelations}
|
||||
selectedRecommendRelations={selectedRecommendRelations}
|
||||
onNext={onNext}
|
||||
onBack={onBack}
|
||||
onSkip={onSkip}
|
||||
/>
|
||||
</ContainerCard>
|
||||
</SimpleLayout>
|
||||
|
||||
@@ -21,4 +21,8 @@
|
||||
line-height: 1.2;
|
||||
height: auto;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.adm-onboarding-btn {
|
||||
width: 130px;
|
||||
}
|
||||
@@ -1,9 +1,5 @@
|
||||
import { Manifest } from '@/utils/data/type';
|
||||
import {
|
||||
MetricData,
|
||||
ModelData,
|
||||
ViewData,
|
||||
} from '@/utils/data/model';
|
||||
import { MetricData, ModelData, ViewData } from '@/utils/data/model';
|
||||
|
||||
export interface AdaptedData
|
||||
extends Omit<Manifest, 'models' | 'metrics' | 'views'> {
|
||||
|
||||
@@ -5,7 +5,7 @@ export const getJoinTypeText = (type) =>
|
||||
[JOIN_TYPE.MANY_TO_ONE]: 'Many-to-one',
|
||||
[JOIN_TYPE.ONE_TO_MANY]: 'One-to-many',
|
||||
[JOIN_TYPE.ONE_TO_ONE]: 'One-to-one',
|
||||
}[type] || 'Unknown');
|
||||
})[type] || 'Unknown';
|
||||
|
||||
export const getCachePeriodText = (period) =>
|
||||
({
|
||||
@@ -13,4 +13,4 @@ export const getCachePeriodText = (period) =>
|
||||
[CACHED_PERIOD.HOUR]: 'hour(s)',
|
||||
[CACHED_PERIOD.MINUTE]: 'minute(s)',
|
||||
[CACHED_PERIOD.SECOND]: 'second(s)',
|
||||
}[period] || 'Unknown');
|
||||
})[period] || 'Unknown';
|
||||
|
||||
@@ -46,26 +46,26 @@ export class MetricData {
|
||||
(column) =>
|
||||
new MetricColumnData(
|
||||
column as MetricColumn,
|
||||
metricType as METRIC_TYPE
|
||||
)
|
||||
metricType as METRIC_TYPE,
|
||||
),
|
||||
),
|
||||
];
|
||||
}, []);
|
||||
|
||||
this.measures = this.columns.filter(
|
||||
(column) => column.metricType === METRIC_TYPE.MEASURE
|
||||
(column) => column.metricType === METRIC_TYPE.MEASURE,
|
||||
);
|
||||
this.timeGrains = this.columns.filter(
|
||||
(column) => column.metricType === METRIC_TYPE.TIME_GRAIN
|
||||
(column) => column.metricType === METRIC_TYPE.TIME_GRAIN,
|
||||
);
|
||||
this.dimensions = !isCumulative
|
||||
? this.columns.filter(
|
||||
(column) => column.metricType === METRIC_TYPE.DIMENSION
|
||||
(column) => column.metricType === METRIC_TYPE.DIMENSION,
|
||||
)
|
||||
: undefined;
|
||||
this.windows = isCumulative
|
||||
? this.columns.filter(
|
||||
(column) => column.metricType === METRIC_TYPE.WINDOW
|
||||
(column) => column.metricType === METRIC_TYPE.WINDOW,
|
||||
)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { JOIN_TYPE, NODE_TYPE } from '@/utils/enum';
|
||||
import {
|
||||
Relationship,
|
||||
ModelColumn,
|
||||
Model,
|
||||
Manifest,
|
||||
} from '@/utils/data/type';
|
||||
import { Relationship, ModelColumn, Model, Manifest } from '@/utils/data/type';
|
||||
|
||||
export class ModelData {
|
||||
public readonly nodeType: NODE_TYPE = NODE_TYPE.MODEL;
|
||||
@@ -39,19 +34,19 @@ export class ModelData {
|
||||
this.refreshTime = model.refreshTime;
|
||||
this.relations = data.relationships
|
||||
.filter((relationship) =>
|
||||
relationship.models.includes(this.referenceName)
|
||||
relationship.models.includes(this.referenceName),
|
||||
)
|
||||
.map((relationship) => new RelationData(relationship));
|
||||
|
||||
this.columns = model.columns.map(
|
||||
(column) => new ModelColumnData(column, model, this.relations)
|
||||
(column) => new ModelColumnData(column, model, this.relations),
|
||||
);
|
||||
this.fields = this.columns.filter(
|
||||
(column) => !column.isCalculated && !column.relation
|
||||
(column) => !column.isCalculated && !column.relation,
|
||||
);
|
||||
this.relationFields = this.columns.filter((column) => column.relation);
|
||||
this.calculatedFields = this.columns.filter(
|
||||
(column) => column.isCalculated && !column.relation
|
||||
(column) => column.isCalculated && !column.relation,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -76,7 +71,7 @@ export class ModelColumnData {
|
||||
this.type = column.type;
|
||||
if (column?.relationship) {
|
||||
const relation = relations.find(
|
||||
(item) => item.referenceName === column?.relationship
|
||||
(item) => item.referenceName === column?.relationship,
|
||||
);
|
||||
this.relation = relation;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ export class ViewData {
|
||||
this.properties = view.properties;
|
||||
|
||||
this.fields = (view.columns || []).map(
|
||||
(column) => new ViewColumnData(column)
|
||||
(column) => new ViewColumnData(column),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
export enum DATA_SOURCES {
|
||||
BIG_QUERY = 'bigQuery',
|
||||
DATA_BRICKS = 'dataBricks',
|
||||
SNOWFLAKE = 'snowflake',
|
||||
TRINO = 'trino',
|
||||
BIG_QUERY = 'BIG_QUERY',
|
||||
DUCKDB = 'DUCK_DB',
|
||||
PG_SQL = 'PG_SQL',
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
export enum Path {
|
||||
Exploration = '/exploration',
|
||||
HOME = '/home',
|
||||
Modeling = '/modeling',
|
||||
Onboarding = '/setup',
|
||||
Home = '/home',
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ export enum SETUP {
|
||||
STARTER = 'starter',
|
||||
CREATE_DATA_SOURCE = 'createDataSource',
|
||||
SELECT_MODELS = 'selectModels',
|
||||
CREATE_MODELS = 'createModels',
|
||||
RECOMMEND_RELATIONS = 'recommendRelations',
|
||||
DEFINE_RELATIONS = 'defineRelations',
|
||||
}
|
||||
|
||||
@@ -6,11 +6,8 @@ export const ERROR_TEXTS = {
|
||||
PROJECT_ID: {
|
||||
REQUIRED: 'Please input project id.',
|
||||
},
|
||||
DATASET: {
|
||||
REQUIRED: 'Please input dataset name.',
|
||||
},
|
||||
LOCATION: {
|
||||
REQUIRED: 'Please input data location.',
|
||||
DATASET_ID: {
|
||||
REQUIRED: 'Please input dataset ID.',
|
||||
},
|
||||
CREDENTIAL: {
|
||||
REQUIRED: 'Please upload credential.',
|
||||
@@ -60,9 +57,6 @@ export const ERROR_TEXTS = {
|
||||
},
|
||||
},
|
||||
ADD_RELATION: {
|
||||
NAME: {
|
||||
REQUIRED: 'Please input name.',
|
||||
},
|
||||
FROM_FIELD: {
|
||||
REQUIRED: 'Please select a field.',
|
||||
},
|
||||
@@ -112,9 +106,6 @@ export const ERROR_TEXTS = {
|
||||
TABLE: {
|
||||
REQUIRED: 'Please select at least one table.',
|
||||
},
|
||||
FIELDS: {
|
||||
REQUIRED: 'Please select at least one field.',
|
||||
},
|
||||
},
|
||||
SAVE_AS_VIEW: {
|
||||
DISPLAY_NAME: {
|
||||
|
||||
Reference in New Issue
Block a user