[FE UI] Fix some bugs about new FE UI (#4830)

1. Add a search boxer in the left tree view of Playground.
2. Fix some visual bugs of UI.
3. Fix bugs that link failed in QueryProfile view.
4. Fix bugs that cookie is always invalid.
5. Set cookie to HTTP_ONLY to make it more safe.
This commit is contained in:
鹏翔
2020-11-05 20:30:09 +08:00
committed by GitHub
parent f239f44b37
commit c5b034acc4
16 changed files with 196 additions and 73 deletions

View File

@ -106,6 +106,7 @@ public class BaseController {
Cookie cookie = new Cookie(PALO_SESSION_ID, key);
cookie.setMaxAge(PALO_SESSION_EXPIRED_TIME);
cookie.setPath("/");
cookie.setHttpOnly(true);
response.addCookie(cookie);
LOG.debug("add session cookie: {} {}", PALO_SESSION_ID, key);
HttpAuthManager.getInstance().addSessionValue(key, value);
@ -170,6 +171,7 @@ public class BaseController {
for (Cookie cookie : cookies) {
if (cookie.getName() != null && cookie.getName().equals(cookieName)) {
cookie.setMaxAge(age);
cookie.setPath("/");
response.addCookie(cookie);
LOG.debug("get update cookie: {} {}", cookie.getName(), cookie.getValue());
}

View File

@ -47,5 +47,7 @@
"tips":"Tips",
"fileSizeWarning": "File size cannot exceed 100M",
"selectWarning": "Please select a table",
"executionTime": "Execution Time"
"executionTime": "Execution Time",
"search":"Search",
"executionFailed":"Execution failed"
}

View File

@ -47,5 +47,7 @@
"tips": "提示",
"fileSizeWarning": "文件大小不能超过100m",
"selectWarning": "请选择表",
"executionTime": "执行时间"
"executionTime": "执行时间",
"search":"查询",
"executionFailed": "执行失败"
}

View File

@ -50,7 +50,6 @@ export default function SortFilterTable(props: any) {
<FilterFilled/>
</Popover>:''}
<Table
rowKey={allTableData?.column_names?.[0]}
columns={localColumns}
dataSource={tableData}
scroll={{x:true}}

View File

@ -30,18 +30,19 @@ function getLinkItem(text, record, index, isInner, item, hrefColumn, path){
if (record.__hrefPaths[hrefColumn.indexOf(item)].includes('http')) {
return <a href={record.__hrefPaths[hrefColumn.indexOf(item)]} target="_blank">{text}</a>;
}
return <Link to={path+location.search+'/'+text}>{text}</Link>;
return <Link to={path+(location.search?location.search:isInner)+'/'+text}>{text}</Link>;
}
return text === '\\N' ? '-' : text;
}
export function getColumns(params: string[], isSort: boolean, isInner, hrefColumn, path) {
if(!params||params.length === 0){return [];}
let arr = params.map(item=> {
let arr = params.map((item,idx)=> {
if (isSort) {
return {
title: item,
dataIndex: item,
className: 'pr-25',
key: item+idx,
sorter: (a,b)=>sortItems(a, b, item),
render:(text, record, index)=>getLinkItem(text,record, index, isInner, item, hrefColumn, path),
};
@ -50,6 +51,7 @@ export function getColumns(params: string[], isSort: boolean, isInner, hrefColum
title: item,
dataIndex: item,
className: 'pr-25',
key: item+idx,
render:(text, record, index)=>getLinkItem(text, record, index, isInner, item, hrefColumn, path),
};
});

View File

@ -91,7 +91,7 @@ function Layouts(props: any) {
);
return (
<Layout>
<Header style={{position: 'fixed', zIndex: 1, width: '100%'}}>
<Header style={{position: 'fixed', zIndex: 99, width: '100%'}}>
<div className={styles['logo']} onClick={()=>{history.replace('/home');setCurrent('')}}></div>
<span className='userSet'>
<Button style={{'color':'#000'}} type="text" size='small' onClick={changeLanguage}>{localStorage.getItem('I18N_LANGUAGE') === 'zh-CN' ? 'English' : '中文'}</Button>

View File

@ -20,26 +20,68 @@
import React,{useState,useEffect} from 'react';
import {AdHocAPI} from 'Src/api/api';
import {getDbName} from 'Utils/utils';
import {Row, Empty} from 'antd';
import {Row, Empty, notification, Table} from 'antd';
import {FlatBtn} from 'Components/flatbtn';
import {useTranslation} from 'react-i18next';
export function DataPrev(props: any) {
let { t } = useTranslation();
const {db_name,tbl_name} = getDbName();
const [tableData,setTableData] = useState([]);
const [columns,setColumns] = useState([]);
function toQuery(): void {
if (!tbl_name){
notification.error({message: t('selectWarning')});
return;
}
AdHocAPI.doQuery({
db_name,
body:{stmt:`SELECT * FROM ${db_name}.${tbl_name} LIMIT 10`},
}).then(res=>{
if (res && res.msg === 'success') {
setTableData(res.data);
console.log(getColumns(res.data?.meta),2222)
setColumns(getColumns(res.data?.meta))
setTableData(getTabledata(res.data));
}
})
.catch(()=>{
setTableData([]);
});
}
function getColumns(params: string[]) {
console.log(params,2222)
if(!params||params.length === 0){return [];}
let arr = params.map(item=> {
return {
title: item.name,
dataIndex: item.name,
key: item.name,
width: 150,
render:(text, record, index)=>{return text === '\\N' ? '-' : text}
};
});
return arr;
}
function getTabledata(data){
let meta = data.meta;
let source = data.data;
let res = [];
if(!source||source.length === 0){return [];}
let metaArr = meta.map(item=>item.name)
for (let i=0;i<source.length;i++) {
let node = source[i];
if(node.length !== meta.length){
return {}
}
let obj = {}
metaArr.map((item,idx)=>{
obj[item] = node[idx]
})
obj['key'] = i
res.push(obj)
}
return res;
}
useEffect(()=>{
toQuery();
},[location.pathname]);
@ -58,42 +100,16 @@ export function DataPrev(props: any) {
{t('refresh')}
</FlatBtn>
</Row>
<div
className="ant-table ant-table-small ant-table-bordered"
style={{marginTop: 10}}
>
{tableData?.meta?.length
?<div className="ant-table-container" >
<div className='ant-table-content'>
<table style={{width: '100%'}}>
<thead className="ant-table-thead">
<tr>
{tableData?.meta?.map(item => (
<th className="ant-table-cell" key={item.name}>
{item.name}
</th>
))}
</tr>
</thead>
<tbody className="ant-table-tbody">
{tableData.data?.map((item,index) => (
<tr className="ant-table-row" key={index}>
{item.map((tdData,index) => (
<td
className="ant-table-cell"
key={tdData+index}
>
{tdData == '\\N'?'-':tdData}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</div>:<Empty image={Empty.PRESENTED_IMAGE_SIMPLE}/>}
</div>
</div>
<Table
bordered
columns={columns}
style={{maxWidth:' calc(100vw - 350px)'}}
// scroll={{ x:'500', y: '36vh'}}
scroll={{ x: 1500, y: 300 }}
dataSource={tableData}
size="small"
/>
</div>
);
}

View File

@ -123,7 +123,7 @@ export function AdhocContentResult(props) {
) : (
<TextWithIcon
icon={<CloseCircleFilled/>}
text={"执行失败: "+runningQueryInfo.msg +' '+ runningQueryInfo.data}
text={`${t('executionFailed')}: `+runningQueryInfo.msg +' '+ runningQueryInfo.data}
color="red"
style={{
marginBottom: 10,
@ -141,7 +141,7 @@ export function AdhocContentResult(props) {
</Row> */}
<Row justify="start">
<Col span={3}>{t('executionTime')}:</Col>
<Col>{runningQueryInfo.data?.time + ' ms'}</Col>
<Col>{(runningQueryInfo.data?.time?runningQueryInfo.data?.time:0) + ' ms'}</Col>
</Row>
{/* <Row justify="start">
<Col span={3}>{t('endTime')}:</Col>

View File

@ -86,6 +86,7 @@ export function ContentStructure(props: any) {
bordered
rowKey='Field'
columns={columns}
scroll={{ y: '36vh' }}
loading={{
spinning: getTableInfoRequest.loading,
delay: TABLE_DELAY,

View File

@ -23,6 +23,7 @@ under the License. */
&-operator {
margin-top: 10px;
}
width: calc(100vw - 350px);
}
.adhoc-content-favorite {

View File

@ -206,7 +206,7 @@ export function AdHocContent(props: any) {
<Route
path={`${match.path}/${AdhocContentRouteKeyEnum.Structure}/:table`}
render={props => (
<div style={{display:'flex',height:'300px'}}>
<div style={{display:'flex',height:'53vh'}}>
<div style={{flex:3}}>
<ContentStructure
queryTable={tableName =>

View File

@ -19,5 +19,5 @@ under the License. */
flex: 0 0 300px;
height: 100%;
background: #fff;
min-height: calc(100vh - 60px);
min-height: calc(100vh - 64px);
}

View File

@ -34,7 +34,8 @@ export function PageSide(props: any) {
<div className={styles['side']} >
<ResizableBox
width={sideBoxWidth}
height={800}
height={Infinity}
style={{'minHeight':'calc(100vh - 64px)','overflow':'hidden'}}
resizeHandles={['e']}
onResizeStart={onResize}
minConstraints={[300, 300]}

View File

@ -0,0 +1,3 @@
.site-tree-search-value {
color: #f50;
}

View File

@ -16,11 +16,12 @@
* specific language governing permissions and limitations
* under the License.
*/
import React, {useState,useEffect} from 'react';
import {Tree, Spin, Space} from 'antd';
import {TableOutlined, HddOutlined} from '@ant-design/icons';
import {Tree, Spin, Input} from 'antd';
const { Search } = Input;
import {TableOutlined, HddOutlined, ReloadOutlined} from '@ant-design/icons';
import {AdHocAPI} from 'Src/api/api';
import {useTranslation} from 'react-i18next';
import {
AdhocContentRouteKeyEnum,
} from '../adhoc.data';
@ -30,53 +31,57 @@ interface DataNode {
isLeaf?: boolean;
children?: DataNode[];
}
import './index.css';
const initTreeDate: DataNode[] = [];
function updateTreeData(list: DataNode[], key: React.Key, children: DataNode[]): DataNode[] {
function updateTreeData(list: DataNode[], key, children) {
return list.map(node => {
if (node.key === key) {
return {
...node,
children,
};
} else if (node.children) {
return {
...node,
children: updateTreeData(node.children, key, children),
};
}
}
return node;
});
}
export function AdHocTree(props: any) {
let { t } = useTranslation();
const [treeData, setTreeData] = useState(initTreeDate);
const [realTree, setRealTree] = useState(initTreeDate);
const [loading, setLoading] = useState(true);
const [expandedKeys, setExpandedKeys] = useState([]);
const [searchValue, setSearchValue] = useState('');
const [autoExpandParent, setAutoExpandParent] = useState(true);
useEffect(() => {
initTreeData()
}, []);
function initTreeData(){
AdHocAPI.getDatabaseList().then(res=>{
if (res.msg === 'success' && Array.isArray(res.data)) {
const num = Math.random()
const treeData = res.data.map((item,index)=>{
return {
title: item,
key: `1-${index}-${item}`,
key: `${num}-1-${index}-${item}`,
icon: <HddOutlined/>,
};
});
setTreeData(treeData);
getRealTree(treeData);
}
setLoading(false);
});
}, []);
}
function onLoadData({key, children}) {
const [storey, index, db_name, tbl_name] = key.split('-');
const [random, storey, index, db_name] = key.split('-');
const param = {
db_name,
tbl_name,
// tbl_name,
};
return AdHocAPI.getDatabaseList(param).then(res=>{
if (res.msg=='success' && Array.isArray(res.data)) {
const treeData = res.data.map((item,index)=>{
if(storey==1){
const children = res.data.map((item,index)=>{
if (storey === '1'){
return {
title: item,
key: `2-${index}-${param.db_name}-${item}`,
@ -86,9 +91,9 @@ export function AdHocTree(props: any) {
}
});
setTreeData(origin =>
updateTreeData(origin, key, treeData),
);
const trData = updateTreeData(treeData, key, children);
setTreeData(trData);
getRealTree(trData);
}
});
@ -102,14 +107,103 @@ export function AdHocTree(props: any) {
props.history.push(`/Playground/${path}/${keys[0].split(':')[1]}`);
}
}
function onSearch(e){
const { value } = e.target;
const expandedKeys = treeData
.map((item, index) => {
if (getParentKey(value, treeData[index].children, index)) {
return item.key
} else {
return null;
}
})
setExpandedKeys(expandedKeys);
setSearchValue(value);
setAutoExpandParent(true);
getRealTree(treeData, value);
};
function onExpand(expandedKeys) {
setExpandedKeys(expandedKeys);
setAutoExpandParent(false);
};
const getParentKey = (key, tree, idx) => {
if (!tree) {
return false;
}
for (let i = 0; i < tree.length; i++) {
const node = tree[i];
if (node.title.includes(key)) {
return true
} else {
treeData[idx].children ? treeData[idx].children[i].title = node.title : ''
}
}
return false;
};
function getRealTree(treeData, value){
const realTree = inner(treeData);
function inner(treeData){
return treeData.map(item => {
const search = value || '';
const index = item.title.indexOf(search);
const beforeStr = item.title.substr(0, index);
const afterStr = item.title.substr(index + search.length);
const title =
index > -1 ? (
<span>
{beforeStr}
<span className="site-tree-search-value">{search}</span>
{afterStr}
</span>
) : (
item.title
);
if (item.children) {
return {...item, title, children: inner(item.children)};
}
return {
...item,
title
};
});
}
debounce(setRealTree(realTree),300);
}
function debounce(fn, wait) {
var timer = null;
return function () {
var context = this
var args = arguments
if (timer) {
clearTimeout(timer);
timer = null;
}
timer = setTimeout(function () {
fn.apply(context, args)
}, wait)
}
}
return (
<>
<Spin spinning={loading} size="small"/>
<div>
<Search
size="small"
style={{ padding: 5, position: 'fixed', zIndex: '99', width: '300px'}}
placeholder={t('search')}
enterButton={<ReloadOutlined />}
onSearch={initTreeData}
onChange={onSearch} />
</div>
<Tree
showIcon={true}
loadData={onLoadData}
treeData={treeData}
style={{'width':'100%', height:'100%' ,paddingTop:'15px'}}
treeData={realTree}
onExpand={onExpand}
expandedKeys={expandedKeys}
autoExpandParent={autoExpandParent}
style={{'width':'100%', height:'86vh' ,paddingTop:'35px',overflowY:'scroll'}}
onSelect={(selectedKeys, info) =>
handleTreeSelect(
selectedKeys,

View File

@ -86,7 +86,7 @@ export default function QueryProfile(params: any) {
:<Table
isSort={true}
isFilter={true}
isInner={true}
isInner={'/QueryProfile'}
allTableData={allTableData}
/>
}