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:
Freda Lai
2024-03-20 11:54:04 +08:00
committed by GitHub
parent 6a76da858b
commit 6c478e0070
28 changed files with 314 additions and 674 deletions
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"
+8 -25
View File
@@ -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)));
+10 -178
View File
@@ -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',
},
];
+25 -28
View File
@@ -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: [],
},
];
+2 -7
View File
@@ -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>
);
+3 -8
View File
@@ -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 -5
View File
@@ -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'> {
+2 -2
View File
@@ -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';
+6 -6
View File
@@ -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;
}
+6 -11
View File
@@ -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;
}
+1 -1
View File
@@ -27,7 +27,7 @@ export class ViewData {
this.properties = view.properties;
this.fields = (view.columns || []).map(
(column) => new ViewColumnData(column)
(column) => new ViewColumnData(column),
);
}
}
+3 -4
View File
@@ -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 -1
View File
@@ -1,6 +1,6 @@
export enum Path {
Exploration = '/exploration',
HOME = '/home',
Modeling = '/modeling',
Onboarding = '/setup',
Home = '/home',
}
-1
View File
@@ -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',
}
+2 -11
View File
@@ -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: {