| @@ -198,6 +198,17 @@ export default [ | |||||
| }, | }, | ||||
| ], | ], | ||||
| }, | }, | ||||
| { | |||||
| name: '代码配置', | |||||
| path: 'codeConfig', | |||||
| routes: [ | |||||
| { | |||||
| name: '代码配置', | |||||
| path: '', | |||||
| component: './CodeConfig/List', | |||||
| }, | |||||
| ], | |||||
| }, | |||||
| ], | ], | ||||
| }, | }, | ||||
| { | { | ||||
| @@ -10,6 +10,12 @@ export enum CommonTabKeys { | |||||
| Public = 'Public', // 公开 | Public = 'Public', // 公开 | ||||
| } | } | ||||
| // 公开还是私有 | |||||
| export enum AvailableRange { | |||||
| Public = 1, // 公开 | |||||
| Private = 0, // 私有 | |||||
| } | |||||
| // 实验状态 | // 实验状态 | ||||
| export enum ExperimentStatus { | export enum ExperimentStatus { | ||||
| Running = 'Running', // 运行中 | Running = 'Running', // 运行中 | ||||
| @@ -0,0 +1,47 @@ | |||||
| .code-config-list { | |||||
| display: flex; | |||||
| flex: 1; | |||||
| flex-direction: column; | |||||
| height: 100%; | |||||
| height: 100%; | |||||
| padding: 20px 0; | |||||
| background: white; | |||||
| box-shadow: 0px 3px 6px rgba(146, 146, 146, 0.09); | |||||
| &__header { | |||||
| display: flex; | |||||
| align-items: center; | |||||
| justify-content: space-between; | |||||
| height: 32px; | |||||
| margin-bottom: 30px; | |||||
| padding: 0 30px; | |||||
| color: @text-color; | |||||
| font-size: 15px; | |||||
| } | |||||
| &__content { | |||||
| display: flex; | |||||
| flex: 1; | |||||
| flex-wrap: wrap; | |||||
| gap: 20px; | |||||
| align-content: flex-start; | |||||
| width: 100%; | |||||
| margin-bottom: 30px; | |||||
| padding: 0 30px; | |||||
| overflow-y: auto; | |||||
| } | |||||
| &__empty { | |||||
| display: flex; | |||||
| flex: 1; | |||||
| align-items: center; | |||||
| justify-content: center; | |||||
| } | |||||
| :global { | |||||
| .ant-pagination { | |||||
| margin-right: 30px; | |||||
| text-align: right; | |||||
| } | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,173 @@ | |||||
| import KFIcon from '@/components/KFIcon'; | |||||
| import { deleteCodeConfigReq, getCodeConfigListReq } from '@/services/codeConfig'; | |||||
| import { openAntdModal } from '@/utils/modal'; | |||||
| import { to } from '@/utils/promise'; | |||||
| import { modalConfirm } from '@/utils/ui'; | |||||
| import { App, Button, Empty, Input, Pagination, PaginationProps } from 'antd'; | |||||
| import { useEffect, useState } from 'react'; | |||||
| import AddCodeConfigModal, { OperationType } from '../components/AddCodeConfigModal'; | |||||
| import CodeConfigItem from '../components/CodeConfigItem'; | |||||
| import styles from './index.less'; | |||||
| // 代码配置数据 | |||||
| export type CodeConfigData = { | |||||
| id: number; | |||||
| code_repo_name: string; | |||||
| code_repo_vis: number; | |||||
| git_url: string; | |||||
| git_branch: string; | |||||
| git_user_name: string; | |||||
| git_password: string; | |||||
| ssh_key: string; | |||||
| verify_mode: number; | |||||
| create_by: string; | |||||
| create_time: string; | |||||
| update_by: string; | |||||
| update_time: string; | |||||
| }; | |||||
| export type ResourceListRef = { | |||||
| reset: () => void; | |||||
| }; | |||||
| function CodeConfigList() { | |||||
| const [dataList, setDataList] = useState<CodeConfigData[]>([]); | |||||
| const [total, setTotal] = useState(0); | |||||
| const [pagination, setPagination] = useState<PaginationProps>({ | |||||
| current: 1, | |||||
| pageSize: 20, | |||||
| }); | |||||
| const [searchText, setSearchText] = useState<string | undefined>(undefined); | |||||
| const [inputText, setInputText] = useState<string | undefined>(undefined); | |||||
| const { message } = App.useApp(); | |||||
| useEffect(() => { | |||||
| getDataList(); | |||||
| }, [pagination, searchText]); | |||||
| // 获取数据请求 | |||||
| const getDataList = async () => { | |||||
| const params = { | |||||
| page: pagination.current! - 1, | |||||
| size: pagination.pageSize, | |||||
| code_repo_name: searchText !== '' ? searchText : undefined, | |||||
| }; | |||||
| const [res] = await to(getCodeConfigListReq(params)); | |||||
| if (res && res.data && res.data.content) { | |||||
| setDataList(res.data.content); | |||||
| setTotal(res.data.totalElements); | |||||
| } | |||||
| }; | |||||
| // 删除请求 | |||||
| const deleteRecord = async (id: number) => { | |||||
| const [res] = await to(deleteCodeConfigReq(id)); | |||||
| if (res) { | |||||
| getDataList(); | |||||
| message.success('删除成功'); | |||||
| } | |||||
| }; | |||||
| // 搜索 | |||||
| const handleSearch = (value: string) => { | |||||
| setSearchText(value); | |||||
| }; | |||||
| // 删除 | |||||
| const handleRemove = (record: CodeConfigData) => { | |||||
| modalConfirm({ | |||||
| title: '确定删除这个代码配置吗?', | |||||
| onOk: () => { | |||||
| deleteRecord(record.id); | |||||
| }, | |||||
| }); | |||||
| }; | |||||
| // 修改 | |||||
| const handleClick = (record: CodeConfigData) => { | |||||
| const { close } = openAntdModal(AddCodeConfigModal, { | |||||
| opType: OperationType.Update, | |||||
| codeConfigData: record, | |||||
| onOk: () => { | |||||
| getDataList(); | |||||
| close(); | |||||
| }, | |||||
| }); | |||||
| }; | |||||
| // 新建 | |||||
| const createCodeConfig = () => { | |||||
| const { close } = openAntdModal(AddCodeConfigModal, { | |||||
| opType: OperationType.Create, | |||||
| onOk: () => { | |||||
| getDataList(); | |||||
| close(); | |||||
| }, | |||||
| }); | |||||
| }; | |||||
| // 分页切换 | |||||
| const handlePageChange: PaginationProps['onChange'] = (page, pageSize) => { | |||||
| setPagination({ | |||||
| current: page, | |||||
| pageSize: pageSize, | |||||
| }); | |||||
| }; | |||||
| return ( | |||||
| <div className={styles['code-config-list']}> | |||||
| <div className={styles['code-config-list__header']}> | |||||
| <span>数据总数:{total}个</span> | |||||
| <div> | |||||
| <Input.Search | |||||
| placeholder="按代码仓库名称筛选" | |||||
| allowClear | |||||
| onSearch={handleSearch} | |||||
| style={{ | |||||
| width: 300, | |||||
| }} | |||||
| onChange={(e) => setInputText(e.target.value)} | |||||
| value={inputText} | |||||
| /> | |||||
| <Button | |||||
| type="default" | |||||
| style={{ marginLeft: '20px' }} | |||||
| onClick={createCodeConfig} | |||||
| icon={<KFIcon type="icon-xinjian2" />} | |||||
| > | |||||
| 新建代码配置 | |||||
| </Button> | |||||
| </div> | |||||
| </div> | |||||
| {dataList?.length !== 0 ? ( | |||||
| <> | |||||
| <div className={styles['code-config-list__content']}> | |||||
| {dataList?.map((item) => ( | |||||
| <CodeConfigItem | |||||
| item={item} | |||||
| key={item.id} | |||||
| onRemove={handleRemove} | |||||
| onClick={handleClick} | |||||
| /> | |||||
| ))} | |||||
| </div> | |||||
| <Pagination | |||||
| total={total} | |||||
| showSizeChanger | |||||
| defaultPageSize={20} | |||||
| pageSizeOptions={[20, 40, 60, 80, 100]} | |||||
| showQuickJumper | |||||
| onChange={handlePageChange} | |||||
| {...pagination} | |||||
| /> | |||||
| </> | |||||
| ) : ( | |||||
| <div className={styles['code-config-list__empty']}> | |||||
| <Empty></Empty> | |||||
| </div> | |||||
| )} | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default CodeConfigList; | |||||
| @@ -0,0 +1,244 @@ | |||||
| import KFModal from '@/components/KFModal'; | |||||
| import { AvailableRange } from '@/enums'; | |||||
| import { type CodeConfigData } from '@/pages/CodeConfig/List'; | |||||
| import { addCodeConfigReq, updateCodeConfigReq } from '@/services/codeConfig'; | |||||
| import { to } from '@/utils/promise'; | |||||
| import { Form, Input, Radio, message, type ModalProps } from 'antd'; | |||||
| import { omit } from 'lodash'; | |||||
| export enum VerifyMode { | |||||
| Password = 0, // 用户名密码 | |||||
| SSH = 1, // SSH Key | |||||
| } | |||||
| export enum OperationType { | |||||
| Create = 0, // 新建 | |||||
| Update = 1, // 更新 | |||||
| } | |||||
| type FormData = Partial<CodeConfigData>; | |||||
| interface AddCodeConfigModalProps extends Omit<ModalProps, 'onOk'> { | |||||
| opType: OperationType; | |||||
| codeConfigData?: CodeConfigData; | |||||
| onOk: () => void; | |||||
| } | |||||
| function AddCodeConfigModal({ opType, codeConfigData, onOk, ...rest }: AddCodeConfigModalProps) { | |||||
| // 上传请求 | |||||
| const createCodeConfig = async (formData: FormData) => { | |||||
| const params: FormData & { id?: number } = { | |||||
| ...formData, | |||||
| }; | |||||
| // 清除多余的信息 | |||||
| if (formData.code_repo_vis === AvailableRange.Public) { | |||||
| omit(params, ['verify_mode', 'git_user_name', 'git_password', 'ssh_key']); | |||||
| } | |||||
| if (formData.verify_mode === VerifyMode.Password) { | |||||
| omit(params, ['ssh_key']); | |||||
| } else if (formData.verify_mode === VerifyMode.SSH) { | |||||
| omit(params, ['git_user_name', 'git_password']); | |||||
| } | |||||
| if (opType === OperationType.Update) { | |||||
| params.id = codeConfigData?.id; | |||||
| } | |||||
| const request = opType === OperationType.Create ? addCodeConfigReq : updateCodeConfigReq; | |||||
| const [res] = await to(request(params)); | |||||
| if (res) { | |||||
| message.success(opType === OperationType.Create ? '创建成功' : '修改成功'); | |||||
| onOk?.(); | |||||
| } | |||||
| }; | |||||
| // 提交 | |||||
| const onFinish = (formData: FormData) => { | |||||
| createCodeConfig(formData); | |||||
| }; | |||||
| // 设置初始值 | |||||
| const initialValues: FormData = codeConfigData ?? { | |||||
| code_repo_vis: AvailableRange.Public, | |||||
| verify_mode: VerifyMode.Password, | |||||
| }; | |||||
| if (initialValues.verify_mode === undefined || initialValues.verify_mode === null) { | |||||
| initialValues.verify_mode = VerifyMode.Password; | |||||
| } | |||||
| return ( | |||||
| <KFModal | |||||
| {...rest} | |||||
| title="新建代码配置" | |||||
| image={require('@/assets/img/create-experiment.png')} | |||||
| width={825} | |||||
| okButtonProps={{ | |||||
| htmlType: 'submit', | |||||
| form: 'form', | |||||
| }} | |||||
| destroyOnClose | |||||
| > | |||||
| <Form | |||||
| name="form" | |||||
| layout="vertical" | |||||
| onFinish={onFinish} | |||||
| initialValues={initialValues} | |||||
| autoComplete="off" | |||||
| > | |||||
| {/* 禁止 Chrome 自动填充 */} | |||||
| {/* <Input type="text" style={{ display: 'none' }} /> | |||||
| <Input type="password" style={{ display: 'none' }} /> */} | |||||
| <Form.Item | |||||
| label="代码仓库名称" | |||||
| name="code_repo_name" | |||||
| required | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入代码仓库名称', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <Input placeholder="请输入代码仓库名称" showCount allowClear maxLength={64} /> | |||||
| </Form.Item> | |||||
| <Form.Item | |||||
| label="代码仓库可见性" | |||||
| name="code_repo_vis" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请选择代码仓库可见性', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <Radio.Group> | |||||
| <Radio value={AvailableRange.Public}>公开</Radio> | |||||
| <Radio value={AvailableRange.Private}>私有</Radio> | |||||
| </Radio.Group> | |||||
| </Form.Item> | |||||
| <Form.Item | |||||
| label="Git 地址" | |||||
| name="git_url" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入 Git 地址', | |||||
| }, | |||||
| { | |||||
| type: 'url', | |||||
| message: '请输入正确的 Git 地址', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <Input placeholder="请输入 Git 地址" showCount allowClear maxLength={256} /> | |||||
| </Form.Item> | |||||
| <Form.Item | |||||
| label="代码分支/Tag" | |||||
| name="git_branch" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入代码分支/Tag', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <Input placeholder="请输入代码分支/Tag" showCount allowClear maxLength={64} /> | |||||
| </Form.Item> | |||||
| <Form.Item | |||||
| noStyle | |||||
| shouldUpdate={(prevValues, currentValues) => | |||||
| prevValues?.code_repo_vis !== currentValues?.code_repo_vis | |||||
| } | |||||
| > | |||||
| {({ getFieldValue }) => { | |||||
| return getFieldValue('code_repo_vis') === AvailableRange.Private ? ( | |||||
| <> | |||||
| <Form.Item | |||||
| label="验证方式" | |||||
| name="verify_mode" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请选择验证方式', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <Radio.Group> | |||||
| <Radio value={VerifyMode.Password}>用户名/密码</Radio> | |||||
| <Radio value={VerifyMode.SSH}>SSH Key</Radio> | |||||
| </Radio.Group> | |||||
| </Form.Item> | |||||
| <Form.Item | |||||
| noStyle | |||||
| shouldUpdate={(prevValues, currentValues) => | |||||
| prevValues?.verify_mode !== currentValues?.verify_mode | |||||
| } | |||||
| > | |||||
| {({ getFieldValue }) => { | |||||
| return getFieldValue('verify_mode') === VerifyMode.Password ? ( | |||||
| <> | |||||
| <Form.Item | |||||
| label="Git 用户名" | |||||
| name="git_user_name" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入 Git 用户名', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <Input | |||||
| placeholder="请输入 Git 用户名" | |||||
| autoComplete="off" | |||||
| showCount | |||||
| allowClear | |||||
| maxLength={64} | |||||
| /> | |||||
| </Form.Item> | |||||
| <Form.Item | |||||
| label="Git 密码" | |||||
| name="git_password" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入 Git 密码', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <Input.Password | |||||
| autoComplete="new-password" | |||||
| placeholder="请输入 Git 密码" | |||||
| allowClear | |||||
| /> | |||||
| </Form.Item> | |||||
| </> | |||||
| ) : ( | |||||
| <Form.Item | |||||
| label="SSH Key" | |||||
| name="ssh_key" | |||||
| rules={[ | |||||
| { | |||||
| required: true, | |||||
| message: '请输入 SSH Key', | |||||
| }, | |||||
| ]} | |||||
| > | |||||
| <Input.TextArea | |||||
| placeholder="请输入 SSH Key" | |||||
| showCount | |||||
| maxLength={1024} | |||||
| autoSize={{ minRows: 3, maxRows: 6 }} | |||||
| allowClear | |||||
| /> | |||||
| </Form.Item> | |||||
| ); | |||||
| }} | |||||
| </Form.Item> | |||||
| </> | |||||
| ) : null; | |||||
| }} | |||||
| </Form.Item> | |||||
| </Form> | |||||
| </KFModal> | |||||
| ); | |||||
| } | |||||
| export default AddCodeConfigModal; | |||||
| @@ -0,0 +1,68 @@ | |||||
| .code-config-item { | |||||
| position: relative; | |||||
| width: calc(25% - 15px); | |||||
| padding: 20px; | |||||
| background: white; | |||||
| border: 1px solid #eaeaea; | |||||
| border-radius: 4px; | |||||
| cursor: pointer; | |||||
| @media screen and (max-width: 1860px) { | |||||
| & { | |||||
| width: calc(33.33% - 13.33px); | |||||
| } | |||||
| } | |||||
| &__name { | |||||
| position: relative; | |||||
| display: inline-block; | |||||
| height: 24px; | |||||
| margin: 0 10px 0 0 !important; | |||||
| color: @text-color; | |||||
| font-size: 16px; | |||||
| } | |||||
| &__url { | |||||
| margin-bottom: 10px; | |||||
| color: @text-color-secondary; | |||||
| font-size: 14px; | |||||
| } | |||||
| &__description { | |||||
| height: 44px; | |||||
| margin-bottom: 20px; | |||||
| color: @text-color-secondary; | |||||
| font-size: 14px; | |||||
| .multiLine(2); | |||||
| } | |||||
| &__time { | |||||
| display: flex; | |||||
| flex: 0 1 content; | |||||
| align-items: center; | |||||
| width: 100%; | |||||
| color: #808080; | |||||
| font-size: 13px; | |||||
| } | |||||
| &:hover { | |||||
| border-color: @primary-color; | |||||
| box-shadow: 0px 0px 6px 1px rgba(0, 0, 0, 0.1); | |||||
| .resource-item__name { | |||||
| color: @primary-color; | |||||
| } | |||||
| } | |||||
| } | |||||
| .resource-item__name { | |||||
| &::after { | |||||
| position: absolute; | |||||
| top: 14px; | |||||
| left: 0; | |||||
| width: 100%; | |||||
| height: 6px; | |||||
| background: linear-gradient(to right, rgba(22, 100, 255, 0.3) 0, rgba(22, 100, 255, 0) 100%); | |||||
| content: ''; | |||||
| } | |||||
| } | |||||
| @@ -0,0 +1,51 @@ | |||||
| import clock from '@/assets/img/clock.png'; | |||||
| import creatByImg from '@/assets/img/creatBy.png'; | |||||
| import KFIcon from '@/components/KFIcon'; | |||||
| import { type CodeConfigData } from '@/pages/CodeConfig/List'; | |||||
| import { formatDate } from '@/utils/date'; | |||||
| import { Button, Flex, Typography } from 'antd'; | |||||
| import styles from './index.less'; | |||||
| type CodeConfigItemProps = { | |||||
| item: CodeConfigData; | |||||
| onClick: (item: CodeConfigData) => void; | |||||
| onRemove: (item: CodeConfigData) => void; | |||||
| }; | |||||
| function CodeConfigItem({ item, onClick, onRemove }: CodeConfigItemProps) { | |||||
| return ( | |||||
| <div className={styles['code-config-item']} onClick={() => onClick(item)}> | |||||
| <Flex justify="space-between" align="center" style={{ marginBottom: '20px', height: '32px' }}> | |||||
| <Typography.Paragraph | |||||
| className={styles['code-config-item__name']} | |||||
| ellipsis={{ tooltip: item.code_repo_name }} | |||||
| > | |||||
| {item.code_repo_name} | |||||
| </Typography.Paragraph> | |||||
| <Button | |||||
| type="text" | |||||
| shape="circle" | |||||
| onClick={(e) => { | |||||
| e.stopPropagation(); | |||||
| onRemove(item); | |||||
| }} | |||||
| > | |||||
| <KFIcon type="icon-shanchu" font={17} /> | |||||
| </Button> | |||||
| </Flex> | |||||
| <div className={styles['code-config-item__description']}>{item.git_url}</div> | |||||
| <Flex justify="space-between"> | |||||
| <div className={styles['code-config-item__time']}> | |||||
| <img style={{ width: '17px', marginRight: '6px' }} src={creatByImg} alt="" /> | |||||
| <span>{item.create_by}</span> | |||||
| </div> | |||||
| <div className={styles['code-config-item__time']}> | |||||
| <img style={{ width: '12px', marginRight: '5px' }} src={clock} alt="" /> | |||||
| <span>最近更新: {formatDate(item.update_time, 'YYYY-MM-DD')}</span> | |||||
| </div> | |||||
| </Flex> | |||||
| </div> | |||||
| ); | |||||
| } | |||||
| export default CodeConfigItem; | |||||
| @@ -9,7 +9,7 @@ import { App, Button, Input, Pagination, PaginationProps } from 'antd'; | |||||
| import { Ref, forwardRef, useEffect, useImperativeHandle, useState } from 'react'; | import { Ref, forwardRef, useEffect, useImperativeHandle, useState } from 'react'; | ||||
| import { CategoryData, ResourceData, ResourceType, resourceConfig } from '../../config'; | import { CategoryData, ResourceData, ResourceType, resourceConfig } from '../../config'; | ||||
| import AddDatasetModal from '../AddDatasetModal'; | import AddDatasetModal from '../AddDatasetModal'; | ||||
| import ResourceItem from '../Resourcetem'; | |||||
| import ResourceItem from '../ResourceItem'; | |||||
| import styles from './index.less'; | import styles from './index.less'; | ||||
| export type ResourceListRef = { | export type ResourceListRef = { | ||||
| @@ -161,7 +161,7 @@ function ResourceList( | |||||
| <span>数据总数:{total}个</span> | <span>数据总数:{total}个</span> | ||||
| <div> | <div> | ||||
| <Input.Search | <Input.Search | ||||
| placeholder="按数据名称筛选" | |||||
| placeholder={`按${config.name}名称筛选`} | |||||
| allowClear | allowClear | ||||
| onSearch={handleSearch} | onSearch={handleSearch} | ||||
| style={{ | style={{ | ||||
| @@ -166,7 +166,7 @@ function EditorList() { | |||||
| key: 'name', | key: 'name', | ||||
| width: '30%', | width: '30%', | ||||
| render: (text, record) => | render: (text, record) => | ||||
| record.url ? ( | |||||
| record.url && record.status === DevEditorStatus.Running ? ( | |||||
| <a className="kf-table-row-link" onClick={(e) => gotoEditorPage(e, record)}> | <a className="kf-table-row-link" onClick={(e) => gotoEditorPage(e, record)}> | ||||
| {text} | {text} | ||||
| </a> | </a> | ||||
| @@ -0,0 +1,39 @@ | |||||
| import { request } from '@umijs/max'; | |||||
| // 分页查询代码配置 | |||||
| export function getCodeConfigListReq(params) { | |||||
| return request(`/api/mmp/codeConfig`, { | |||||
| method: 'GET', | |||||
| params, | |||||
| }); | |||||
| } | |||||
| // 新增代码配置 | |||||
| export function addCodeConfigReq(data) { | |||||
| return request(`/api/mmp/codeConfig`, { | |||||
| method: 'POST', | |||||
| data, | |||||
| }); | |||||
| } | |||||
| // 更新代码配置 | |||||
| export function updateCodeConfigReq(data) { | |||||
| return request(`/api/mmp/codeConfig`, { | |||||
| method: 'PUT', | |||||
| data, | |||||
| }); | |||||
| } | |||||
| // 删除代码配置 | |||||
| export function deleteCodeConfigReq(id) { | |||||
| return request(`/api/mmp/codeConfig/${id}`, { | |||||
| method: 'DELETE', | |||||
| }); | |||||
| } | |||||
| // 查询代码配置详情 | |||||
| export function getCodeConfigDetailReq(id) { | |||||
| return request(`/api/mmp/codeConfig/${id}`, { | |||||
| method: 'GET' | |||||
| }); | |||||
| } | |||||