Commit a6ad2058 authored by godkaikai's avatar godkaikai

执行历史

parent a3f5b303
......@@ -28,14 +28,14 @@ public class StudioExecuteDTO {
private String savePointPath;
public JobConfig getJobConfig() {
return new JobConfig(useResult, useSession, getSession(), useRemote, clusterId, taskId, jobName, fragment, maxRowNum, checkPoint, parallelism, savePointPath);
return new JobConfig(useResult, useSession, session, useRemote, clusterId, taskId, jobName, fragment, maxRowNum, checkPoint, parallelism, savePointPath);
}
public String getSession() {
/*public String getSession() {
if(useRemote) {
return clusterId + "_" + session;
}else{
return "0_" + session;
}
}
}*/
}
......@@ -28,7 +28,7 @@ public class Job2MysqlHandler implements JobHandler {
history.setClusterId(job.getJobConfig().getClusterId());
history.setJobManagerAddress(job.getJobManagerAddress());
history.setJobName(job.getJobConfig().getJobName());
history.setSession(job.getJobConfig().getSessionKey());
history.setSession(job.getJobConfig().getSession());
history.setStatus(job.getStatus().ordinal());
history.setStartTime(job.getStartTime());
history.setTaskId(job.getJobConfig().getTaskId());
......@@ -54,9 +54,10 @@ public class Job2MysqlHandler implements JobHandler {
History history = new History();
history.setId(job.getId());
history.setJobId(job.getJobId());
history.setStatement(job.getStatement());
history.setStatus(job.getStatus().ordinal());
history.setEndTime(job.getEndTime());
history.setResult(JSONUtil.toJsonStr(job.getResult()));
// history.setResult(JSONUtil.toJsonStr(job.getResult()));
historyService.updateById(history);
return true;
}
......
......@@ -38,4 +38,8 @@ public class History implements Serializable {
@TableField(exist = false)
private String statusText;
@TableField(exist = false)
private String clusterAlias;
@TableField(exist = false)
private String taskAlias;
}
......@@ -30,9 +30,11 @@
<select id="selectForProTable" resultType="com.dlink.model.History">
select
a.*
a.*,
(select b.alias FROM dlink_cluster b where b.id=a.cluster_id) as clusterAlias,
(select c.alias FROM dlink_task c where c.id=a.task_id) as taskAlias
from
dlink_cluster a
dlink_history a
<where>
1=1
<if test='param.name!=null and param.name!=""'>
......
......@@ -17,7 +17,7 @@ public class JobConfig {
private boolean useResult;
private boolean useSession;
private String sessionKey;
private String session;
private boolean useRemote;
private Integer clusterId;
private String host;
......@@ -29,12 +29,12 @@ public class JobConfig {
private Integer parallelism;
private String savePointPath;
public JobConfig(boolean useResult, boolean useSession, String sessionKey, boolean useRemote, Integer clusterId,
public JobConfig(boolean useResult, boolean useSession, String session, boolean useRemote, Integer clusterId,
Integer taskId, String jobName, boolean useSqlFragment, Integer maxRowNum, Integer checkpoint,
Integer parallelism, String savePointPath) {
this.useResult = useResult;
this.useSession = useSession;
this.sessionKey = sessionKey;
this.session = session;
this.useRemote = useRemote;
this.clusterId = clusterId;
this.taskId = taskId;
......@@ -46,10 +46,10 @@ public class JobConfig {
this.savePointPath = savePointPath;
}
public JobConfig(boolean useResult, boolean useSession, String sessionKey, boolean useRemote, Integer clusterId) {
public JobConfig(boolean useResult, boolean useSession, String session, boolean useRemote, Integer clusterId) {
this.useResult = useResult;
this.useSession = useSession;
this.sessionKey = sessionKey;
this.session = session;
this.useRemote = useRemote;
this.clusterId = clusterId;
}
......
......@@ -117,12 +117,12 @@ public class JobManager extends RunTime {
private Executor createExecutorWithSession() {
if(config.isUseSession()) {
ExecutorEntity executorEntity = SessionPool.get(config.getSessionKey());
ExecutorEntity executorEntity = SessionPool.get(config.getSession());
if (executorEntity != null) {
executor = executorEntity.getExecutor();
} else {
createExecutor();
SessionPool.push(new ExecutorEntity(config.getSessionKey(), executor));
SessionPool.push(new ExecutorEntity(config.getSession(), executor));
}
}else {
createExecutor();
......@@ -134,7 +134,7 @@ public class JobManager extends RunTime {
public boolean init() {
handler = JobHandler.build();
String host = config.getHost();
if (config.isUseRemote() && host != null && !("").equals(host)) {
if (host != null && !("").equals(host)) {
String[] strs = host.split(NetConstant.COLON);
if (strs.length >= 2) {
jobManagerHost = strs[0];
......
.code{
width: 100%;
max-height: 500px;
display: block;
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: rgb(246, 248, 250);
border-radius: 3px;
}
import {StateType} from "@/pages/FlinkSqlStudio/model";
import {connect} from "umi";
import {Button, Tag, Space} from 'antd';
import {Button, Tag, Space,Typography, Divider, Badge,Drawer,} from 'antd';
import {MessageOutlined,ClusterOutlined,FireOutlined,ReloadOutlined} from "@ant-design/icons";
import { LightFilter, ProFormDatePicker } from '@ant-design/pro-form';
import ProList from '@ant-design/pro-list';
import request from 'umi-request';
import {handleRemove, queryData} from "@/components/Common/crud";
import ProDescriptions from '@ant-design/pro-descriptions';
import {useRef, useState} from "react";
import {DocumentTableListItem} from "@/pages/Document/data";
import ProForm, {
ModalForm,
ProFormText,
ProFormDateRangePicker,
ProFormSelect,
} from '@ant-design/pro-form';
import styles from "./index.less";
import {ActionType} from "@ant-design/pro-table";
import {showJobData} from "@/components/Studio/StudioEvent/DQL";
type GithubIssueItem = {
url: string;
const { Title, Paragraph, Text, Link } = Typography;
type HistoryItem = {
id: number;
number: number;
title: string;
labels: {
name: string;
color: string;
}[];
state: string;
comments: number;
created_at: string;
updated_at: string;
closed_at?: string;
clusterId: number;
clusterAlias: string;
session: string;
jobId: string;
jobName: string;
jobManagerAddress: string;
statusText: string;
status: number;
statement: string;
error: string;
result: string;
config: string;
startTime: string;
endTime: string;
taskId: number;
taskAlias: string;
};
type HistoryConfig={
useSession:boolean;
session:string;
useRemote:boolean;
clusterId:number;
host:string;
useResult:boolean;
maxRowNum:number;
taskId:number;
jobName:string;
useSqlFragment:boolean;
checkpoint:number;
parallelism:number;
savePointPath:string;
};
const url = '/api/history';
const StudioHistory = (props: any) => {
const {current} = props;
const {current,refs,dispatch} = props;
const [modalVisit, setModalVisit] = useState(false);
const [row, setRow] = useState<HistoryItem>();
const [config,setConfig] = useState<HistoryConfig>();
const [type,setType] = useState<number>();
const showDetail=(row:HistoryItem,type:number)=>{
setRow(row);
setModalVisit(true);
setType(type);
setConfig(JSON.parse(row.config));
};
const removeHistory=async (row:HistoryItem)=>{
await handleRemove(url,[row]);
// refs.current?.reloadAndRest?.();
refs.history?.current?.reload();
};
return (
<><ProList<GithubIssueItem>
<>
<ProList<HistoryItem>
actionRef={refs.history}
toolBarRender={() => {
return [
<Button key="3" type="primary">
新建
</Button>,
// <Button key="3" type="primary" icon={<ReloadOutlined />}/>,
];
}}
search={{
filterType: 'light',
}}
rowKey="name"
headerTitle="基础列表"
request={async (params = {}) =>
request<{
data: GithubIssueItem[];
}>('https://proapi.azurewebsites.net/github/issues', {
params,
})
}
rowKey="id"
headerTitle="执行历史"
request={(params, sorter, filter) => queryData(url,{...params, sorter:{id:'descend'}, filter})}
pagination={{
pageSize: 5,
}}
showActions="hover"
metas={{
title: {
dataIndex: 'user',
title: '用户',
},
avatar: {
dataIndex: 'avatar',
search: false,
dataIndex: 'jobId',
title: 'JobId',
render: (_, row) => {
return (
<Space size={0}>
<Tag color="blue" key={row.jobId}>
<FireOutlined /> {row.jobId}
</Tag>
</Space>
);
},
},
description: {
dataIndex: 'title',
search: false,
},
render:(_, row)=>{
let jobConfig = JSON.parse(row.config);
return (<Paragraph>
<blockquote>
<Link href={`http://${jobConfig.host}`} target="_blank">
[{jobConfig.session}:{row.jobManagerAddress}]
</Link>
<Divider type="vertical"/>开始于:{row.startTime}
<Divider type="vertical"/>完成于:{row.endTime}
</blockquote>
</Paragraph>)
}
},
subTitle: {
dataIndex: 'labels',
render: (_, row) => {
return (
<Space size={0}>
{row.labels?.map((label: { name: string }) => (
<Tag color="blue" key={label.name}>
{label.name}
{row.jobName?(
<Tag color="gray" key={row.jobName}>
{row.jobName}
</Tag>
):''}
{row.session?(
<Tag color="orange" key={row.session}>
<MessageOutlined /> {row.session}
</Tag>
))}
):''}
{row.clusterAlias?(
<Tag color="green" key={row.clusterAlias}>
<ClusterOutlined /> {row.clusterAlias}
</Tag>
):(<Tag color="blue" key={row.clusterAlias}>
<ClusterOutlined /> 本地环境
</Tag>)}
{(row.status==2) ?
(<><Badge status="success"/><Text type="success">SUCCESS</Text></>):
(row.status==1) ?
<><Badge status="error"/><Text type="secondary">RUNNING</Text></> :
(row.status==3) ?
<><Badge status="error"/><Text type="danger">FAILED</Text></> :
(row.status==4) ?
<><Badge status="error"/><Text type="warning">CANCEL</Text></> :
(row.status==0) ?
<><Badge status="error"/><Text type="warning">INITIALIZE</Text></> :
<><Badge status="success"/><Text type="danger">UNKNOWEN</Text></>}
</Space>
);
},
......@@ -80,44 +166,178 @@ const StudioHistory = (props: any) => {
},
actions: {
render: (text, row) => [
<a href={row.url} target="_blank" rel="noopener noreferrer" key="link">
链路
<a key="config" onClick={()=>{showDetail(row,1)}}>
执行配置
</a>,
<a key="statement" onClick={()=>{showDetail(row,2)}}>
FlinkSql语句
</a>,
<a href={row.url} target="_blank" rel="noopener noreferrer" key="warning">
报警
<a key="result" onClick={()=>{showJobData(row.jobId,dispatch)}}>
预览数据
</a>,
<a href={row.url} target="_blank" rel="noopener noreferrer" key="view">
查看
<a key="error" onClick={()=>{showDetail(row,4)}}>
异常信息
</a>,
<a key="delete" onClick={()=>{removeHistory(row)}}>
删除
</a>,
],
search: false,
},
jobName: {
dataIndex: 'jobName',
title: '作业名',
},
clusterId: {
dataIndex: 'clusterId',
title: '执行方式',
},
session: {
dataIndex: 'session',
title: '共享会话',
},
status: {
// 自己扩展的字段,主要用于筛选,不在列表中显示
title: '状态',
valueType: 'select',
valueEnum: {
all: {text: '全部', status: 'Default'},
open: {
text: '未解决',
status: 'Error',
ALL: {text: '全部', status: 'ALL'},
INITIALIZE: {
text: '初始化',
status: 'INITIALIZE',
},
RUNNING: {
text: '运行中',
status: 'RUNNING',
},
SUCCESS: {
text: '成功',
status: 'SUCCESS',
},
closed: {
text: '已解决',
status: 'Success',
FAILED: {
text: '失败',
status: 'FAILED',
},
processing: {
text: '解决中',
status: 'Processing',
CANCEL: {
text: '停止',
status: 'CANCEL',
},
},
},
startTime: {
dataIndex: 'startTime',
title: '开始时间',
valueType: 'dateTimeRange',
},
endTime: {
dataIndex: 'endTime',
title: '完成时间',
valueType: 'dateTimeRange',
},
}}
options={{
search: false,
setting:false
}}
/>
<ModalForm
// title="新建表单"
visible={modalVisit}
onFinish={async () => {
setRow(undefined);
setType(undefined);
setConfig(undefined);
}}
onVisibleChange={setModalVisit}
submitter={{
submitButtonProps: {
style: {
display: 'none',
},
},
}}
>
{type==1 && (
<ProDescriptions
column={2}
title='执行配置'
>
<ProDescriptions.Item span={2} label="JobId" >
{row.jobId}
</ProDescriptions.Item>
<ProDescriptions.Item label="共享会话" >
{config.useSession?'启用':'禁用'}
</ProDescriptions.Item>
<ProDescriptions.Item label="会话 Key">
{config.session}
</ProDescriptions.Item>
<ProDescriptions.Item label="执行方式" >
{config.useRemote?'远程':'本地'}
</ProDescriptions.Item>
<ProDescriptions.Item label="集群ID">
{config.clusterId}
</ProDescriptions.Item>
<ProDescriptions.Item label="预览结果" >
{config.useResult?'启用':'禁用'}
</ProDescriptions.Item>
<ProDescriptions.Item label="最大行数">
{config.maxRowNum}
</ProDescriptions.Item>
<ProDescriptions.Item span={2} label="JobManagerAddress">
{row.jobManagerAddress}
</ProDescriptions.Item>
<ProDescriptions.Item label="作业ID">
{config.taskId}
</ProDescriptions.Item>
<ProDescriptions.Item label="作业名">
{config.jobName}
</ProDescriptions.Item>
<ProDescriptions.Item label="片段机制">
{config.useSqlFragment?'启用':'禁用'}
</ProDescriptions.Item>
<ProDescriptions.Item label="并行度">
{config.parallelism}
</ProDescriptions.Item>
<ProDescriptions.Item label="CheckPoint">
{config.checkpoint}
</ProDescriptions.Item>
<ProDescriptions.Item label="SavePointPath">
{config.savePointPath}
</ProDescriptions.Item>
</ProDescriptions>
)}
{type==2 && (
<ProDescriptions
column={1}
title='FlinkSql 语句'
>
<ProDescriptions.Item label="JobId" >
{row.jobId}
</ProDescriptions.Item>
<ProDescriptions.Item>
<pre className={styles.code}>{row.statement}</pre>
</ProDescriptions.Item>
</ProDescriptions>
)}
{type==4 && (
<ProDescriptions
column={1}
title='异常信息'
>
<ProDescriptions.Item label="JobId" >
{row.jobId}
</ProDescriptions.Item>
<ProDescriptions.Item >
<pre className={styles.code}>{row.error}</pre>
</ProDescriptions.Item>
</ProDescriptions>
)}
</ModalForm>
</>
);
};
export default connect(({Studio}: {Studio: StateType}) => ({
current: Studio.current,
refs: Studio.refs,
}))(StudioHistory);
import {Typography, Divider, Badge, Empty} from "antd";
import {StateType} from "@/pages/FlinkSqlStudio/model";
import {connect} from "umi";
const { Title, Paragraph, Text, Link } = Typography;
const StudioHistory2 = (props:any) => {
const {current} = props;
return (
<Typography>
{current.console.result.map((item)=> {
return (<Paragraph>
<blockquote><Link href={`http://${item.flinkHost}:${item.flinkPort}`} target="_blank">
[{item.sessionId}:{item.flinkHost}:{item.flinkPort}]
</Link> <Divider type="vertical" />{item.finishDate}
<Divider type="vertical" />
{!item.success ? <><Badge status="error"/><Text type="danger">Error</Text></> :
<><Badge status="success"/><Text type="success">Success</Text></>}
<Divider type="vertical" />
{item.jobName&&<Text code>{item.jobName}</Text>}
{item.jobId&&<Text code>{item.jobId}</Text>}
<Text keyboard>{item.time}ms</Text></blockquote>
{item.statement && (<pre style={{height:'40px'}}>{item.statement}</pre>)}
{item.msg ? item.msg : ''}
{item.error && (<pre style={{height:'100px'}}>{item.error}</pre>)}
</Paragraph>)
})}
{current.console.result.length==0?<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />:''}
</Typography>
);
};
export default connect(({ Studio }: { Studio: StateType }) => ({
current: Studio.current,
}))(StudioHistory2);
......@@ -5,6 +5,7 @@ import {useState} from "react";
// import Highlighter from 'react-highlight-words';
import { SearchOutlined } from '@ant-design/icons';
import {showJobData} from "@/components/Studio/StudioEvent/DQL";
import ProTable from '@ant-design/pro-table';
const { Option } = Select;
const { Title, Paragraph, Text, Link } = Typography;
......@@ -96,36 +97,11 @@ const StudioTable = (props:any) => {
return datas;
};
const onChange=(val:string)=>{
showJobData(val,dispatch);
};
return (
<Typography>
<Form.Item label="当前执行记录" tooltip="选择最近的执行记录,仅包含成功的记录">
<Select
style={{ width: '100%' }}
placeholder="选择最近的执行记录"
optionLabelProp="label"
onChange={onChange}
>
{current.console.result.map((item,index)=> {
if(item.status=='SUCCESS'&&item.jobId) {
let tag = (<> <Tooltip placement="topLeft" title={item.statement}>
<Tag color="processing">{item.startTime}</Tag>
<Tag color="processing">{item.endTime}</Tag>
<Text underline>[{item.jobConfig.sessionKey}:{item.jobConfig.host}]</Text>
{item.jobConfig.jobName&&<Text code>{item.jobConfig.jobName}</Text>}
{item.jobId&&<Text code>{item.jobId}</Text>}
{item.statement}</Tooltip></>);
return (<Option value={item.jobId} label={tag}>
{tag}
</Option>)
}
})}
</Select>
</Form.Item>
{result&&result.jobId&&!result.isDestroyed?(<Table dataSource={result.rowData} columns={getColumns(result.columns)} />):(<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />)}
{result&&result.jobId&&!result.isDestroyed?
(<ProTable dataSource={result.rowData} columns={getColumns(result.columns)} search={false}
/>):(<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />)}
</Typography>
);
};
......
import {Tabs, Empty} from "antd";
import {BarsOutlined,DatabaseOutlined,AppstoreOutlined,ClusterOutlined,ApiOutlined,FireOutlined,FunctionOutlined} from "@ant-design/icons";
import {BarsOutlined,DatabaseOutlined,AppstoreOutlined,ClusterOutlined,MessageOutlined,FireOutlined,FunctionOutlined} from "@ant-design/icons";
import {StateType} from "@/pages/FlinkSqlStudio/model";
import {connect} from "umi";
import styles from "./index.less";
......@@ -27,7 +27,7 @@ const StudioLeftTool = (props:any) => {
<TabPane tab={<span><ClusterOutlined /> 集群</span>} key="Cluster" >
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
</TabPane>
<TabPane tab={<span><ApiOutlined /> 连接器</span>} key="Connectors" >
<TabPane tab={<span><MessageOutlined /> 会话</span>} key="Connectors" >
<StudioConnector />
</TabPane>
<TabPane tab={<span><FireOutlined /> 任务</span>} key="FlinkTask" >
......
......@@ -13,6 +13,7 @@ import { postDataArray} from "@/components/Common/crud";
import {executeSql} from "@/pages/FlinkSqlStudio/service";
import StudioHelp from "./StudioHelp";
import {showTables} from "@/components/Studio/StudioEvent/DDL";
import {timeout} from "d3-timer";
const menu = (
<Menu>
......@@ -23,7 +24,7 @@ const menu = (
const StudioMenu = (props: any) => {
const {tabs,current,currentPath,form,dispatch} = props;
const {tabs,current,currentPath,form,refs,dispatch} = props;
const execute = () => {
let selectsql =null;
......@@ -57,6 +58,9 @@ const StudioMenu = (props: any) => {
key:taskKey,
icon: <SmileOutlined style={{ color: '#108ee9' }} />,
});
setTimeout(()=>{
refs?.history?.current?.reload();
},2000);
const result = executeSql(param);
result.then(res=>{
notification.close(taskKey);
......@@ -284,5 +288,6 @@ export default connect(({Studio}: { Studio: StateType }) => ({
current: Studio.current,
currentPath: Studio.currentPath,
tabs: Studio.tabs,
refs: Studio.refs,
// monaco: Studio.monaco,
}))(StudioMenu);
......@@ -71,6 +71,51 @@ const StudioConfig = (props: any) => {
>
<Input placeholder="自定义作业名" />
</Form.Item>
<Row>
<Col span={10}>
<Form.Item
label="共享会话" className={styles.form_item} name="useSession" valuePropName="checked"
tooltip={{ title: '开启共享会话,将进行 Flink Catalog 的共享', icon: <InfoCircleOutlined /> }}
>
<Switch checkedChildren="启用" unCheckedChildren="禁用"
/>
</Form.Item>
</Col>
<Col span={14}>
<Form.Item
label="会话 Key" tooltip="设置共享会话的 Key" name="session"
className={styles.form_item}>
<Select
placeholder="选择会话"
allowClear
onChange={onChangeClusterSession}
dropdownRender={menu => (
<div>
{menu}
<Divider style={{ margin: '4px 0' }} />
<div style={{ display: 'flex', flexWrap: 'nowrap', padding: 8 }}>
<Input style={{ flex: 'auto' }} value={newSesstion}
onChange={(e)=>{
setNewSesstion(e.target.value);
}}
/>
<a
style={{ flex: 'none', padding: '8px', display: 'block', cursor: 'pointer' }}
onClick={addSession}
>
<PlusOutlined />
</a>
</div>
</div>
)}
>
{session.map(item => (
<Option key={item}>{item}</Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
<Row>
<Col span={12}>
<Form.Item
......@@ -97,51 +142,6 @@ const StudioConfig = (props: any) => {
<Switch checkedChildren="启用" unCheckedChildren="禁用"
/>
</Form.Item>
<Row>
<Col span={10}>
<Form.Item
label="共享会话" className={styles.form_item} name="useSession" valuePropName="checked"
tooltip={{ title: '开启共享会话,将进行 Flink Catalog 的共享', icon: <InfoCircleOutlined /> }}
>
<Switch checkedChildren="启用" unCheckedChildren="禁用"
/>
</Form.Item>
</Col>
<Col span={14}>
<Form.Item
label="会话 Key" tooltip="设置共享会话的 Key" name="session"
className={styles.form_item}>
<Select
placeholder="选择会话"
allowClear
onChange={onChangeClusterSession}
dropdownRender={menu => (
<div>
{menu}
<Divider style={{ margin: '4px 0' }} />
<div style={{ display: 'flex', flexWrap: 'nowrap', padding: 8 }}>
<Input style={{ flex: 'auto' }} value={newSesstion}
onChange={(e)=>{
setNewSesstion(e.target.value);
}}
/>
<a
style={{ flex: 'none', padding: '8px', display: 'block', cursor: 'pointer' }}
onClick={addSession}
>
<PlusOutlined />
</a>
</div>
</div>
)}
>
{session.map(item => (
<Option key={item}>{item}</Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
</Form>
</>
);
......
......@@ -24,8 +24,23 @@
padding: 0px;
}
/* --- card 内偏移样式 --- start */
/* --- list toolbar隐藏滚动条 --- start */
.ant-pro-table-list-toolbar {
overflow-x: hidden;
overflow-y: hidden;
line-height: 1;
}
/* --- list toolbar隐藏滚动条 --- start */
/* --- list toolbar修改内偏移 --- start */
.ant-pro-table-list-toolbar-container {
padding: 0;
}
/* --- list toolbar修改内偏移 --- start */
/* --- prodescription item宽度 --- start */
.ant-descriptions-item-content {
width: 100%;
}
/* --- prodescription item宽度 --- start */
}
/* --- tabs 垂直样式 --- start */
......
......@@ -93,6 +93,9 @@ export type StateType = {
session: string[];
result:{};
rightClickMenu?: boolean;
refs:{
history:any;
};
};
export type ModelType = {
......@@ -196,7 +199,10 @@ const Model: ModelType = {
},
session: [],
result:{},
rightClickMenu: false
rightClickMenu: false,
refs:{
history:{},
}
},
effects: {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment