diff --git a/src/api/componentDeploy.ts b/src/api/componentDeploy.ts new file mode 100644 index 0000000..5fcef26 --- /dev/null +++ b/src/api/componentDeploy.ts @@ -0,0 +1,9 @@ +import axios from 'axios'; + +// 公共路径 +const urlPrefix = '/api/v1/bpms-workbench'; + +// 部署列表 +export function getDeployList(params) { + return axios.get(`${urlPrefix}/componentDeploy/list`, { params }); +} \ No newline at end of file diff --git a/src/components/EditableTable/index.tsx b/src/components/EditableTable/index.tsx new file mode 100644 index 0000000..ab81d37 --- /dev/null +++ b/src/components/EditableTable/index.tsx @@ -0,0 +1,280 @@ +import React, { useState, useRef, useEffect, useContext, useCallback } from 'react'; +import { Button, Table, Input, Select, Form, FormInstance, Switch, DatePicker } from '@arco-design/web-react'; + +const FormItem = Form.Item; +const EditableContext = React.createContext<{ getForm?: () => FormInstance }>({}); + +interface EditableTableProps { + columns: ColumnProps[]; + data: any[]; + onDataChange?: (data: any[]) => void; + showAddButton?: boolean; + addButtonText?: string; + onAdd?: () => void; + showDeleteButton?: boolean; + deleteButtonText?: string; + onDelete?: (key: string) => void; + // Table组件的其他配置属性 + tableProps?: any; + // 添加空数据的配置 + emptyItem?: any; +} + +interface ColumnProps { + title: string; + dataIndex: string; + editable?: boolean; + render?: (text: any, record: any, index: number) => React.ReactNode; + // 用于指定编辑时使用的组件类型 + editComponent?: 'input' | 'select' | 'switch' | 'date' | 'custom'; + // 用于select组件的选项 + options?: { label: string; value: any }[]; + // 用于自定义渲染编辑组件 + renderEdit?: (props: EditComponentProps) => React.ReactNode; + + // 其他列配置属性 + [key: string]: any; +} + +interface EditComponentProps { + value: any; + onChange: (value: any) => void; + rowData: any; + column: ColumnProps; +} + +interface EditableRowProps { + children: React.ReactNode; + record: any; + className: string; + + [key: string]: any; +} + +interface EditableCellProps { + children: React.ReactNode; + className: string; + rowData: any; + column: ColumnProps; + onHandleSave: (row: any) => void; +} + +function EditableRow(props: EditableRowProps) { + const { children, record, className, ...rest } = props; + const refForm = useRef(null); + + const getForm = () => refForm.current; + + return ( + +
+ {children} +
+
+ ); +} + +function EditableCell(props: EditableCellProps) { + const { children, className, rowData, column, onHandleSave } = props; + const { getForm } = useContext(EditableContext); + + const cellValueChangeHandler = (value: any) => { + const values = { + [column.dataIndex]: value + }; + onHandleSave && onHandleSave({ ...rowData, ...values }); + }; + + // 渲染编辑组件 + const renderEditComponent = () => { + // 如果有自定义渲染函数,使用它 + if (column.renderEdit) { + return column.renderEdit({ + value: rowData[column.dataIndex], + onChange: cellValueChangeHandler, + rowData, + column + }); + } + + // 根据editComponent类型渲染不同的组件 + switch (column.editComponent) { + case 'select': + return ( + cellValueChangeHandler(value)} + style={{ width: '100%' }} + /> + ); + } + }; + + // 如果列是可编辑的,直接渲染编辑组件 + if (column.editable) { + return ( +
+ + {renderEditComponent()} + +
+ ); + } + + // 非编辑状态直接显示文本 + return ( +
+ {children} +
+ ); +} + +const EditableTable: React.FC = ({ + columns, + data, + onDataChange, + showAddButton = true, + addButtonText = 'Add', + onAdd, + showDeleteButton = true, + deleteButtonText = 'Delete', + onDelete, + tableProps = {}, + emptyItem = {} + }) => { + const [tableData, setTableData] = useState(data); + + useEffect(() => { + setTableData(data); + }, [data]); + + const handleSave = (row: any) => { + const newData = [...tableData]; + const index = newData.findIndex((item) => row.key === item.key); + if (index !== -1) { + newData.splice(index, 1, { ...newData[index], ...row }); + setTableData(newData); + onDataChange && onDataChange(newData); + } + }; + + const removeRow = (key: string) => { + const newData = tableData.filter((item) => item.key !== key); + setTableData(newData); + onDataChange && onDataChange(newData); + onDelete && onDelete(key); + }; + + const addRow = () => { + if (onAdd) { + onAdd(); + } + else { + // 添加一条空数据 + const newKey = `${Date.now()}`; + const newRow = { + key: newKey, + ...emptyItem + }; + const newData = [...tableData, newRow]; + setTableData(newData); + onDataChange && onDataChange(newData); + } + }; + + // 处理列配置,添加删除按钮 + const processedColumns = [...columns]; + if (showDeleteButton && !columns.some(col => col.dataIndex === 'op')) { + processedColumns.push({ + title: 'Operation', + dataIndex: 'op', + render: (_: any, record: any) => ( + + ) + }); + } + + return ( + <> + {showAddButton && ( + + )} + + column.editable + ? { + ...column, + onCell: () => ({ + onHandleSave: handleSave + }) + } + : column + )} + className="table-demo-editable-cell" + {...tableProps} + /> + + ); +}; + +export default EditableTable; \ No newline at end of file diff --git a/src/const/isdp/componentDeploy.ts b/src/const/isdp/componentDeploy.ts index 1a455a9..19f1003 100644 --- a/src/const/isdp/componentDeploy.ts +++ b/src/const/isdp/componentDeploy.ts @@ -8,31 +8,36 @@ export const runStatusConstant = { UNKNOWN: 'unknown', RUN: 'run', ERROR: 'error', - STOP: 'stop', + STOP: 'stop' }; export const runStatusDic = [ { label: '可用', value: runStatusConstant.HEALTHY, + color: 'green' }, { label: '未知', value: runStatusConstant.UNKNOWN, + color: 'gray' }, { // 部署 可停止 label: '运行', value: runStatusConstant.RUN, + color: 'blue' }, { label: '异常', value: runStatusConstant.ERROR, + color: 'orange' }, { // 部署 可启用 label: '停止', value: runStatusConstant.STOP, - }, + color: 'red' + } ]; export const runStatusMap = runStatusDic.reduce((obj, item) => { obj[item.value] = item.label; @@ -43,17 +48,17 @@ export const runStatusMap = runStatusDic.reduce((obj, item) => { */ export const startStatusConstant = { RUN: 'run', - STOP: 'stop', + STOP: 'stop' }; export const startStatusDic = [ { label: '启用中', - value: startStatusConstant.RUN, + value: startStatusConstant.RUN }, { label: '已下架', - value: startStatusConstant.STOP, - }, + value: startStatusConstant.STOP + } ]; export const startStatusMap = startStatusDic.reduce((obj, item) => { obj[item.value] = item.label; @@ -73,25 +78,25 @@ export const compileStatusConstant = { NOT_COMPILED: 'not-compiled', get(type) { return compileStatusConstant[type] || compileStatusConstant.NOT_COMPILED; - }, + } }; export const compileStatusDic = [ { label: '编译中', - value: compileStatusConstant.BUILT_DOING, + value: compileStatusConstant.BUILT_DOING }, { label: '免编译', - value: compileStatusConstant.BUILT_IGNORE, + value: compileStatusConstant.BUILT_IGNORE }, { label: '已编译', - value: compileStatusConstant.BUILT_DOCKER, + value: compileStatusConstant.BUILT_DOCKER }, { label: '编译失败', - value: compileStatusConstant.BUILT_FAIL, - }, + value: compileStatusConstant.BUILT_FAIL + } ]; export const compileStatusMap = startStatusDic.reduce((obj, item) => { obj[item.value] = item.label; @@ -102,17 +107,17 @@ export const compileStatusMap = startStatusDic.reduce((obj, item) => { */ export const runTypeConstant = { LOCAL: 0, - ONLINE: 1, + ONLINE: 1 }; export const runTypeDic = [ { label: '本地运行', - value: runTypeConstant.LOCAL, + value: runTypeConstant.LOCAL }, { label: '线上运行', - value: runTypeConstant.ONLINE, - }, + value: runTypeConstant.ONLINE + } ]; export const option = { ...globalOption, @@ -135,14 +140,14 @@ export const option = { label: 'id', prop: 'id', hide: true, - display: false, + display: false }, { label: '标识', prop: 'identifier', type: 'input', overHidden: true, - display: false, + display: false }, { label: '实例名称', @@ -151,8 +156,8 @@ export const option = { overHidden: true, rules: [ { required: true, message: '必须填写实例名称' }, - { max: 50, message: '实例名称不能超过50字符' }, - ], + { max: 50, message: '实例名称不能超过50字符' } + ] }, { label: '运行类型', @@ -160,7 +165,7 @@ export const option = { type: 'input', width: 80, display: false, - dicData: runTypeDic, + dicData: runTypeDic }, { label: '运行状态', @@ -169,14 +174,14 @@ export const option = { width: 80, dicData: runStatusDic, display: false, - slot: true, + slot: true }, { label: '创建时间', prop: 'createTime', type: 'input', // width: 160, - display: false, + display: false }, { label: '实例描述', @@ -184,9 +189,9 @@ export const option = { type: 'textarea', hide: true, minRows: 3, - span: 24, - }, - ], + span: 24 + } + ] }; export const instanceOption = { ...globalOption, @@ -216,20 +221,20 @@ export const instanceOption = { action: '/api/blade-isdp/fileSystem/fileUpload', propsHttp: { res: 'data', - url: 'link', + url: 'link' }, fileType: 'img', limit: 1, fileSize: 1024 * 10, - accept: 'image/png, image/jpeg, image/jpg', + accept: 'image/png, image/jpeg, image/jpg' // tip: '图片尺寸不超过300*300像素,大小限制为1mb以内', // rules: [{ // required: true, // message: "请输入图片地址", // trigger: "blur" // }] - }, - ], + } + ] }; export const envConfigOption = { ...globalOption, @@ -252,11 +257,11 @@ export const envConfigOption = { formSlot: true, rules: [ { required: true, message: '启动项名称不能为空' }, - { max: 50, message: '启动项名称最长为50字符' }, + { max: 50, message: '启动项名称最长为50字符' } ], width: 120, span: 24, - cell: true, + cell: true }, { label: '参数名(key)', @@ -264,10 +269,10 @@ export const envConfigOption = { formSlot: true, rules: [ { required: true, message: '请输入参数名(key)' }, - { max: 50, message: '参数名(key)最长为50字符' }, + { max: 50, message: '参数名(key)最长为50字符' } ], span: 24, - cell: true, + cell: true }, { label: '配置值', @@ -275,7 +280,7 @@ export const envConfigOption = { span: 24, formSlot: true, rules: [{ max: 200, message: '配置值最长为200字符' }], - cell: true, + cell: true }, { label: '描述说明', @@ -284,8 +289,8 @@ export const envConfigOption = { formSlot: true, span: 24, rules: [{ max: 200, message: '描述说明最长为200字符' }], - cell: true, - }, + cell: true + } // { // label: "配置类型", // prop: "unmodifiable", @@ -302,7 +307,7 @@ export const envConfigOption = { // } // ] // }, - ], + ] }; export const addInstanceOption = { ...globalOption, @@ -317,7 +322,7 @@ export const addInstanceOption = { // disabled: true, dicData: [], rules: [{ required: true, message: '请选择环境变量' }], - overHidden: true, + overHidden: true }, { label: '主机', @@ -327,10 +332,10 @@ export const addInstanceOption = { props: { label: 'name', value: 'id', - desc: 'ip', + desc: 'ip' }, rules: [{ required: true, message: '请输入主机' }], - overHidden: true, + overHidden: true }, { label: 'CPU核数', @@ -343,7 +348,7 @@ export const addInstanceOption = { marks: [1, 4, 8].reduce((obj, pre) => { obj[pre] = pre + '核'; return obj; - }, {}), + }, {}) }, // { // label: "容器名称", @@ -362,8 +367,8 @@ export const addInstanceOption = { value: false, dicData: [ { label: '否', value: false }, - { label: '是', value: true }, - ], + { label: '是', value: true } + ] }, { label: '最大内存', @@ -379,9 +384,9 @@ export const addInstanceOption = { 1024: '1GB', 2048: '2GB', 4096: '4GB', - 6144: '6GB', + 6144: '6GB' }, - showStops: true, + showStops: true }, // 网络模式 选择框 { @@ -393,19 +398,19 @@ export const addInstanceOption = { dicData: [ { label: '桥接模式', - value: 'bridge', + value: 'bridge' }, { label: '主机模式', - value: 'host', + value: 'host' }, { label: '禁用网络', - value: 'null', - }, + value: 'null' + } ], rules: [{ required: true, message: '请选择网络模式' }], - overHidden: true, + overHidden: true }, // 网卡 { @@ -415,7 +420,7 @@ export const addInstanceOption = { value: 'bridge', span: 9, overHidden: true, - rules: [{ required: true, message: '请选择网卡' }], + rules: [{ required: true, message: '请选择网卡' }] }, // ip { @@ -425,7 +430,7 @@ export const addInstanceOption = { formSlot: true, value: '', overHidden: true, - span: 9, + span: 9 }, { label: '端口映射', @@ -441,23 +446,23 @@ export const addInstanceOption = { type: 'number', value: '80', max: 65535, - rules: [{ required: true, message: '请输入主机端口' }], + rules: [{ required: true, message: '请输入主机端口' }] }, { label: '容器端口', prop: 'containerPort', type: 'number', max: 65535, - rules: [{ required: true, message: '请输入容器端口' }], + rules: [{ required: true, message: '请输入容器端口' }] }, { label: '备注', prop: 'remake', type: 'input', - value: '', - }, - ], - }, + value: '' + } + ] + } }, { label: '目录挂载', @@ -472,22 +477,22 @@ export const addInstanceOption = { prop: 'hostPath', type: 'input', value: '', - rules: [{ required: true, message: '请输入主机路径' }, patternTrim], + rules: [{ required: true, message: '请输入主机路径' }, patternTrim] }, { label: '容器路径', prop: 'containerPath', type: 'input', value: '', - rules: [{ required: true, message: '请输入容器路径' }, patternTrim], + rules: [{ required: true, message: '请输入容器路径' }, patternTrim] }, { label: '备注', prop: 'remake', - type: 'input', - }, - ], - }, + type: 'input' + } + ] + } }, { // 设备挂载 @@ -503,14 +508,14 @@ export const addInstanceOption = { prop: 'hostPath', type: 'input', value: '', - rules: [{ required: true, message: '请输入主机路径' }, patternTrim], + rules: [{ required: true, message: '请输入主机路径' }, patternTrim] }, { label: '容器路径', prop: 'containerPath', type: 'input', value: '', - rules: [{ required: true, message: '请输入容器路径' }, patternTrim], + rules: [{ required: true, message: '请输入容器路径' }, patternTrim] }, { label: '权限', @@ -520,18 +525,18 @@ export const addInstanceOption = { dicData: [ { value: 'rwm', label: 'rwm' }, { value: 'rw', label: 'rw' }, - { value: 'r', label: 'r' }, + { value: 'r', label: 'r' } ], - rules: [{ required: true, message: '请输入权限' }], + rules: [{ required: true, message: '请输入权限' }] }, { label: '备注', prop: 'remake', type: 'input', - value: '', - }, - ], - }, + value: '' + } + ] + } }, // 重启策略 { @@ -545,18 +550,18 @@ export const addInstanceOption = { dicData: [ { label: '始终', - value: 'always', + value: 'always' }, { label: '失败', - value: 'on-failure', + value: 'on-failure' }, { label: '无', - value: 'no', - }, + value: 'no' + } ], - overHidden: true, - }, - ], + overHidden: true + } + ] }; diff --git a/src/pages/componentDevelopment/componentDeployment/addModal.tsx b/src/pages/componentDevelopment/componentDeployment/addModal.tsx new file mode 100644 index 0000000..6f5a24e --- /dev/null +++ b/src/pages/componentDevelopment/componentDeployment/addModal.tsx @@ -0,0 +1,289 @@ +import React, { useState } from 'react' ; +import { Modal, Form, Select, Grid, Slider, Switch, Input } from '@arco-design/web-react'; +import EditableTable from '@/components/EditableTable'; + +const FormItem = Form.Item; +const Option = Select.Option; + +const runTypes = [ + { + label: '本地运行', + value: 'local' + }, + { + label: '线上运行', + value: 'online' + } +]; + +const portColumns = [ + { + title: '主机端口', + dataIndex: 'name', + editable: true + }, + { + title: '容器端口', + dataIndex: 'salary', + editable: true + }, + { + title: '备注', + dataIndex: 'email', + editable: true + + } +]; +const directoryColumns = [ + { + title: '主机路径', + dataIndex: 'name', + editable: true + }, + { + title: '容器路径', + dataIndex: 'salary', + editable: true + }, + { + title: '备注', + dataIndex: 'email', + editable: true + } +]; +const deviceColumns = [ + { + title: '主机路径', + dataIndex: 'name', + editable: true + }, + { + title: '容器路径', + dataIndex: 'salary', + editable: true + }, + { + title: '权限', + dataIndex: 'email', + editable: true + }, + { + title: '备注', + dataIndex: 'email', + editable: true + } +]; + +const AddModal = ({ visible, setVisible }) => { + const [currentRunType, setCurrentRunType] = useState('local'); + const [tableData, setTableData] = useState([ + { + key: '1', + name: 'Jane Doe', + salary: 23000, + email: 'jane.doe@example.com' + } + ]); + + return ( + setVisible(false)} + onCancel={() => setVisible(false)} + autoFocus={false} + focusLock={true} + style={{ width: '75%' }} + > +
+ + + + + + + + { + currentRunType === 'online' && ( + <> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + setTableData(newData)} + showAddButton={true} + addButtonText="添加行" + showDeleteButton={true} + deleteButtonText="删除" + tableProps={{ + pagination: { pageSize: 5 }, + scroll: { y: 400 } + }} + /> + + + setTableData(newData)} + showAddButton={true} + addButtonText="添加行" + showDeleteButton={true} + deleteButtonText="删除" + tableProps={{ + pagination: { pageSize: 5 }, + scroll: { y: 400 } + }} + /> + + + setTableData(newData)} + showAddButton={true} + addButtonText="添加行" + showDeleteButton={true} + deleteButtonText="删除" + tableProps={{ + pagination: { pageSize: 5 }, + scroll: { y: 400 } + }} + /> + + + ) + } + + +
+ ); +}; + +export default AddModal; \ No newline at end of file diff --git a/src/pages/componentDevelopment/componentDeployment/collapseList.tsx b/src/pages/componentDevelopment/componentDeployment/collapseList.tsx index 2001533..218a019 100644 --- a/src/pages/componentDevelopment/componentDeployment/collapseList.tsx +++ b/src/pages/componentDevelopment/componentDeployment/collapseList.tsx @@ -1,227 +1,113 @@ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { Collapse, Space, Tag, Button, Table, TableColumnProps } from '@arco-design/web-react'; import styles from './style/collapseList.module.less'; -import { IconEye } from '@arco-design/web-react/icon'; +import ListNode from '@/pages/componentDevelopment/componentDeployment/listNode'; +import AddModal from '@/pages/componentDevelopment/componentDeployment/addModal'; +import { getDeployList } from '@/api/componentDeploy'; +import { runStatusConstant, runStatusDic } from '@/const/isdp/componentDeploy'; const CollapseItem = Collapse.Item; +const CollapseList = () => { + const [collapses, setCollapses] = useState([]); + const [visible, setVisible] = useState(false); -const headerNode = () => { - return ( -
- - -
小车零件中心点识别
-
+ const getList = async () => { + const params = { + current: 1, + size: 10 + }; + const res: any = await getDeployList(params); + if (res.code === 200) setCollapses(res.data.records); + }; - - 视觉AI组件 - X86_64 - 启用中 -
- - 4 -
-
-
- ); -}; + useEffect(() => { + getList(); + }, []); -const extraNode = () => { - return ( -
- -
- - 未编译 -
-
- - 重新编译 -
-
- - 新增实例 -
-
- - 下架组件 -
-
- - 更多操作 -
-
-
- ); -}; + const headerNode = (item) => { + const getRunStatus = () => { + return runStatusDic.find((v) => v.value === runStatusConstant[item.runStatus]) || { + color: 'gray', + label: '未知' + }; + }; -const extraNode1 = () => { - return ( -
- -
- - 已编译 -
-
- - 重新编译 -
-
- - 新增实例 -
-
- - 下架组件 -
-
- - 更多操作 -
-
-
- ); -}; + return ( +
+ + +
{item.name}
+
-const columns: TableColumnProps[] = [ - { - title: '#', - dataIndex: 'key', - align: 'center' - }, - { - title: '组件标识', - dataIndex: 'name', - align: 'center' - }, - { - title: '实例名称', - dataIndex: 'salary', - align: 'center' - }, - { - title: '运行类型', - dataIndex: 'address', - align: 'center' - }, - { - title: '运行状态', - dataIndex: 'email', - align: 'center' - }, - { - title: '创建时间', - dataIndex: 'email', - align: 'center' - }, - { - title: '操作', - dataIndex: '', - align: 'center', - render: (col, record, index) => ( -
- - - - - - - - + + {item.componentClassify} + {item.arches} + {getRunStatus().label} +
+ + {item.instanceCount} +
- ) - } -]; -const data = [ - { - key: '1', - name: 'Jane Doe', - salary: 23000, - address: '32 Park Road, London', - email: 'jane.doe@example.com' - }, - { - key: '2', - name: 'Alisa Ross', - salary: 25000, - address: '35 Park Road, London', - email: 'alisa.ross@example.com' - }, - { - key: '3', - name: 'Kevin Sandra', - salary: 22000, - address: '31 Park Road, London', - email: 'kevin.sandra@example.com' - }, - { - key: '4', - name: 'Ed Hellen', - salary: 17000, - address: '42 Park Road, London', - email: 'ed.hellen@example.com' - }, - { - key: '5', - name: 'William Smith', - salary: 27000, - address: '62 Park Road, London', - email: 'william.smith@example.com' - } -]; + ); + }; -const listNode = () => { - return ( -
- ); -}; + const extraNode = () => { + return ( +
+ + {/*
*/} + {/* */} + {/* 未编译*/} + {/*
*/} + {/*
*/} + {/* */} + {/* 重新编译*/} + {/*
*/} +
setVisible(true)}> + + 新增实例 +
+
+ + 下架组件 +
+
+ + 更多操作 +
+
+
+ ); + }; -const CollapseList = () => { return ( -
- - +
+ - {listNode()} - - - - {listNode()} - + {collapses.map((item, index) => ( + + + + ))} + +
- - {listNode()} - -
-
+ + ); }; diff --git a/src/pages/componentDevelopment/componentDeployment/listNode.tsx b/src/pages/componentDevelopment/componentDeployment/listNode.tsx new file mode 100644 index 0000000..757f55a --- /dev/null +++ b/src/pages/componentDevelopment/componentDeployment/listNode.tsx @@ -0,0 +1,112 @@ +import React from "react"; +import { Button, Space, Table, TableColumnProps } from '@arco-design/web-react'; +import styles from '@/pages/componentDevelopment/componentDeployment/style/collapseList.module.less'; + +const ListNode = () => { + + + const columns: TableColumnProps[] = [ + { + title: '#', + dataIndex: 'key', + align: 'center' + }, + { + title: '组件标识', + dataIndex: 'name', + align: 'center' + }, + { + title: '实例名称', + dataIndex: 'salary', + align: 'center' + }, + { + title: '运行类型', + dataIndex: 'address', + align: 'center' + }, + { + title: '运行状态', + dataIndex: 'email', + align: 'center' + }, + { + title: '创建时间', + dataIndex: 'email', + align: 'center' + }, + { + title: '操作', + dataIndex: '', + align: 'center', + render: (col, record, index) => ( +
+ + + + + + + + + +
+ ) + } + ]; + const data = [ + { + key: '1', + name: 'Jane Doe', + salary: 23000, + address: '32 Park Road, London', + email: 'jane.doe@example.com' + }, + { + key: '2', + name: 'Alisa Ross', + salary: 25000, + address: '35 Park Road, London', + email: 'alisa.ross@example.com' + }, + { + key: '3', + name: 'Kevin Sandra', + salary: 22000, + address: '31 Park Road, London', + email: 'kevin.sandra@example.com' + }, + { + key: '4', + name: 'Ed Hellen', + salary: 17000, + address: '42 Park Road, London', + email: 'ed.hellen@example.com' + }, + { + key: '5', + name: 'William Smith', + salary: 27000, + address: '62 Park Road, London', + email: 'william.smith@example.com' + } + ]; + return ( +
+ ) +} + +export default ListNode; \ No newline at end of file