TableList.tsx 20.6 KB
Newer Older
Lijiao's avatar
Lijiao committed
1
2
3
import * as React from 'react';
import axios from 'axios';
import ReactEcharts from 'echarts-for-react';
4
import { Row, Table, Button, Popconfirm, Modal, Checkbox, Select, Icon } from 'antd';
5
import { ColumnProps } from 'antd/lib/table';
6
const Option = Select.Option;
7
const CheckboxGroup = Checkbox.Group;
8
import { MANAGER_IP, trialJobStatus, COLUMN_INDEX, COLUMNPro } from '../../static/const';
9
import { convertDuration, formatTimestamp, intermediateGraphOption, killJob } from '../../static/function';
10
import { TRIALS } from '../../static/datamodel';
11
import { TableRecord } from '../../static/interface';
Lijiao's avatar
Lijiao committed
12
import OpenRow from '../public-child/OpenRow';
Lijiao's avatar
Lijiao committed
13
import Compare from '../Modal/Compare';
14
import '../../static/style/search.scss';
Lijiao's avatar
Lijiao committed
15
16
require('../../static/style/tableStatus.css');
require('../../static/style/logPath.scss');
17
require('../../static/style/search.scss');
Lijiao's avatar
Lijiao committed
18
19
require('../../static/style/table.scss');
require('../../static/style/button.scss');
Lijiao's avatar
Lijiao committed
20
require('../../static/style/openRow.scss');
Lijiao's avatar
Lijiao committed
21
22
23
24
25
26
27
28
29
const echarts = require('echarts/lib/echarts');
require('echarts/lib/chart/line');
require('echarts/lib/component/tooltip');
require('echarts/lib/component/title');
echarts.registerTheme('my_theme', {
    color: '#3c8dbc'
});

interface TableListProps {
30
31
    pageSize: number;
    tableSource: Array<TableRecord>;
32
33
    columnList: Array<string>; // user select columnKeys
    changeColumn: (val: Array<string>) => void;
34
    trialsUpdateBroadcast: number;
Lijiao's avatar
Lijiao committed
35
36
37
38
39
}

interface TableListState {
    intermediateOption: object;
    modalVisible: boolean;
40
41
    isObjFinal: boolean;
    isShowColumn: boolean;
42
    selectRows: Array<TableRecord>;
Lijiao's avatar
Lijiao committed
43
44
    isShowCompareModal: boolean;
    selectedRowKeys: string[] | number[];
45
46
47
    intermediateData: Array<object>; // a trial's intermediate results (include dict)
    intermediateId: string;
    intermediateOtherKeys: Array<string>;
48
49
50
51
52
}

interface ColumnIndex {
    name: string;
    index: number;
Lijiao's avatar
Lijiao committed
53
54
55
56
}

class TableList extends React.Component<TableListProps, TableListState> {

57
58
    public intervalTrialLog = 10;
    public _trialId: string;
59
    public tables: Table<TableRecord> | null;
60

Lijiao's avatar
Lijiao committed
61
62
63
64
65
    constructor(props: TableListProps) {
        super(props);

        this.state = {
            intermediateOption: {},
66
67
68
            modalVisible: false,
            isObjFinal: false,
            isShowColumn: false,
Lijiao's avatar
Lijiao committed
69
70
            isShowCompareModal: false,
            selectRows: [],
71
72
73
74
            selectedRowKeys: [], // close selected trial message after modal closed
            intermediateData: [],
            intermediateId: '',
            intermediateOtherKeys: []
Lijiao's avatar
Lijiao committed
75
76
77
        };
    }

78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
    showIntermediateModal = async (id: string) => {
        const res = await axios.get(`${MANAGER_IP}/metric-data/${id}`);
        if (res.status === 200) {
            const intermediateArr: number[] = [];
            // support intermediate result is dict because the last intermediate result is
            // final result in a succeed trial, it may be a dict.
            // get intermediate result dict keys array
            let otherkeys: Array<string> = ['default'];
            if (res.data.length !== 0) {
                otherkeys = Object.keys(JSON.parse(res.data[0].data));
            }
            // intermediateArr just store default val
            Object.keys(res.data).map(item => {
                const temp = JSON.parse(res.data[item].data);
                if (typeof temp === 'object') {
                    intermediateArr.push(temp.default);
                } else {
                    intermediateArr.push(temp);
Lijiao's avatar
Lijiao committed
96
97
                }
            });
98
            const intermediate = intermediateGraphOption(intermediateArr, id);
Lijiao's avatar
Lijiao committed
99
            this.setState({
100
101
102
103
                intermediateData: res.data, // store origin intermediate data for a trial
                intermediateOption: intermediate,
                intermediateOtherKeys: otherkeys,
                intermediateId: id
Lijiao's avatar
Lijiao committed
104
105
            });
        }
106
        this.setState({ modalVisible: true });
Lijiao's avatar
Lijiao committed
107
108
    }

109
110
    // intermediate button click -> intermediate graph for each trial
    // support intermediate is dict
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
    selectOtherKeys = (value: string) => {

        const isShowDefault: boolean = value === 'default' ? true : false;
        const { intermediateData, intermediateId } = this.state;
        const intermediateArr: number[] = [];
        // just watch default key-val
        if (isShowDefault === true) {
            Object.keys(intermediateData).map(item => {
                const temp = JSON.parse(intermediateData[item].data);
                if (typeof temp === 'object') {
                    intermediateArr.push(temp[value]);
                } else {
                    intermediateArr.push(temp);
                }
            });
        } else {
            Object.keys(intermediateData).map(item => {
                const temp = JSON.parse(intermediateData[item].data);
                if (typeof temp === 'object') {
                    intermediateArr.push(temp[value]);
                }
            });
        }
        const intermediate = intermediateGraphOption(intermediateArr, intermediateId);
        // re-render
136
137
138
        this.setState({
            intermediateOption: intermediate
        });
139
140
    }

Lijiao's avatar
Lijiao committed
141
    hideIntermediateModal = () => {
142
143
144
        this.setState({
            modalVisible: false
        });
Lijiao's avatar
Lijiao committed
145
146
    }

147
    hideShowColumnModal = () => {
148
149
150
        this.setState({
            isShowColumn: false
        });
151
152
153
154
155
    }

    // click add column btn, just show the modal of addcolumn
    addColumn = () => {
        // show user select check button
156
157
158
        this.setState({
            isShowColumn: true
        });
159
160
161
162
    }

    // checkbox for coloumn
    selectedColumn = (checkedValues: Array<string>) => {
163
164
165
        // 9: because have nine common column, 
        // [Intermediate count, Start Time, End Time] is hidden by default
        let count = 9;
166
167
168
169
170
        const want: Array<object> = [];
        const finalKeys: Array<string> = [];
        const wantResult: Array<string> = [];
        Object.keys(checkedValues).map(m => {
            switch (checkedValues[m]) {
Lijiao's avatar
Lijiao committed
171
                case 'Trial No.':
Lijiao's avatar
Lijiao committed
172
                case 'ID':
173
174
                case 'Start Time':
                case 'End Time':
Lijiao's avatar
Lijiao committed
175
176
                case 'Duration':
                case 'Status':
177
178
                case 'Operation':
                case 'Default':
179
                case 'Intermediate result':
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
                    break;
                default:
                    finalKeys.push(checkedValues[m]);
            }
        });

        Object.keys(finalKeys).map(n => {
            want.push({
                name: finalKeys[n],
                index: count++
            });
        });

        Object.keys(checkedValues).map(item => {
            const temp = checkedValues[item];
            Object.keys(COLUMN_INDEX).map(key => {
                const index = COLUMN_INDEX[key];
                if (index.name === temp) {
                    want.push(index);
                }
            });
        });

        want.sort((a: ColumnIndex, b: ColumnIndex) => {
            return a.index - b.index;
        });

        Object.keys(want).map(i => {
            wantResult.push(want[i].name);
        });

211
        this.props.changeColumn(wantResult);
212
213
    }

214
    openRow = (record: TableRecord) => {
Lijiao's avatar
Lijiao committed
215
        return (
216
            <OpenRow trialId={record.id} />
Lijiao's avatar
Lijiao committed
217
218
219
        );
    }

220
221
    fillSelectedRowsTostate = (selected: number[] | string[], selectedRows: Array<TableRecord>) => {
        this.setState({ selectRows: selectedRows, selectedRowKeys: selected });
Lijiao's avatar
Lijiao committed
222
223
224
225
226
227
228
229
    }
    // open Compare-modal
    compareBtn = () => {

        const { selectRows } = this.state;
        if (selectRows.length === 0) {
            alert('Please select datas you want to compare!');
        } else {
230
            this.setState({ isShowCompareModal: true });
Lijiao's avatar
Lijiao committed
231
232
233
234
235
        }
    }
    // close Compare-modal
    hideCompareModal = () => {
        // close modal. clear select rows data, clear selected track
236
        this.setState({ isShowCompareModal: false, selectedRowKeys: [], selectRows: [] });
Lijiao's avatar
Lijiao committed
237
238
239
    }

    render() {
240
241
        const { pageSize, columnList } = this.props;
        const tableSource: Array<TableRecord> = JSON.parse(JSON.stringify(this.props.tableSource));
242
        const { intermediateOption, modalVisible, isShowColumn,
243
            selectRows, isShowCompareModal, selectedRowKeys, intermediateOtherKeys } = this.state;
Lijiao's avatar
Lijiao committed
244
245
        const rowSelection = {
            selectedRowKeys: selectedRowKeys,
246
            onChange: (selected: string[] | number[], selectedRows: Array<TableRecord>) => {
Lijiao's avatar
Lijiao committed
247
248
249
                this.fillSelectedRowsTostate(selected, selectedRows);
            }
        };
250
        let showTitle = COLUMNPro;
251
        const showColumn: Array<object> = [];
252
253
254

        // parameter as table column
        const parameterStr: Array<string> = [];
255
256
257
258
259
260
261
262
        if (tableSource.length > 0) {
            const trialMess = TRIALS.getTrial(tableSource[0].id);
            const trial = trialMess.description.parameters;
            const parameterColumn: Array<string> = Object.keys(trial);
            parameterColumn.forEach(value => {
                parameterStr.push(`${value} (search space)`);
            });
        }
263
264
        showTitle = COLUMNPro.concat(parameterStr);

265
        // only succeed trials have final keys
266
267
        if (tableSource.filter(record => record.status === 'SUCCEEDED').length >= 1) {
            const temp = tableSource.filter(record => record.status === 'SUCCEEDED')[0].accuracy;
268
            if (temp !== undefined && typeof temp === 'object') {
269
270
271
272
273
274
275
276
277
278
279
280
                // concat default column and finalkeys
                const item = Object.keys(temp);
                // item: ['default', 'other-keys', 'maybe loss']
                if (item.length > 1) {
                    const want: Array<string> = [];
                    item.forEach(value => {
                        if (value !== 'default') {
                            want.push(value);
                        }
                    });
                    showTitle = COLUMNPro.concat(want);
                }
281
282
            }
        }
283
        for (const item of columnList) {
284
285
286
287
288
            const paraColumn = item.match(/ \(search space\)$/);
            let cc;
            if (paraColumn !== null) {
                cc = paraColumn.input;
            }
289
            switch (item) {
Lijiao's avatar
Lijiao committed
290
                case 'Trial No.':
291
                    showColumn.push(SequenceIdColumnConfig);
292
                    break;
Lijiao's avatar
Lijiao committed
293
                case 'ID':
294
                    showColumn.push(IdColumnConfig);
295
                    break;
296
297
                case 'Start Time':
                    showColumn.push(StartTimeColumnConfig);
298
                    break;
299
300
                case 'End Time':
                    showColumn.push(EndTimeColumnConfig);
301
                    break;
Lijiao's avatar
Lijiao committed
302
                case 'Duration':
303
                    showColumn.push(DurationColumnConfig);
304
                    break;
Lijiao's avatar
Lijiao committed
305
                case 'Status':
306
                    showColumn.push(StatusColumnConfig);
307
                    break;
308
                case 'Intermediate result':
309
                    showColumn.push(IntermediateCountColumnConfig);
310
                    break;
311
                case 'Default':
312
                    showColumn.push(AccuracyColumnConfig);
313
314
315
316
317
318
                    break;
                case 'Operation':
                    showColumn.push({
                        title: 'Operation',
                        dataIndex: 'operation',
                        key: 'operation',
319
                        width: 120,
320
                        render: (text: string, record: TableRecord) => {
321
                            let trialStatus = record.status;
322
                            const flag: boolean = (trialStatus === 'RUNNING') ? false : true;
323
                            return (
324
325
326
327
328
329
330
331
332
333
334
                                <Row id="detail-button">
                                    {/* see intermediate result graph */}
                                    <Button
                                        type="primary"
                                        className="common-style"
                                        onClick={this.showIntermediateModal.bind(this, record.id)}
                                        title="Intermediate"
                                    >
                                        <Icon type="line-chart" />
                                    </Button>
                                    {/* kill job */}
Lijiao's avatar
Lijiao committed
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
                                    {
                                        flag
                                            ?
                                            <Button
                                                type="default"
                                                disabled={true}
                                                className="margin-mediate special"
                                                title="kill"
                                            >
                                                <Icon type="stop" />
                                            </Button>
                                            :
                                            <Popconfirm
                                                title="Are you sure to cancel this trial?"
                                                okText="Yes"
                                                cancelText="No"
                                                onConfirm={killJob.
                                                    bind(this, record.key, record.id, record.status)}
                                            >
                                                <Button
                                                    type="default"
                                                    disabled={false}
                                                    className="margin-mediate special"
                                                    title="kill"
                                                >
                                                    <Icon type="stop" />
                                                </Button>
                                            </Popconfirm>
                                    }
364
                                </Row>
365
366
367
368
                            );
                        },
                    });
                    break;
369
370
371
                case (cc):
                    // remove SEARCH_SPACE title
                    const realItem = item.replace(' (search space)', '');
372
                    showColumn.push({
373
374
375
376
                        title: realItem,
                        dataIndex: item,
                        key: item,
                        width: '6%',
377
                        render: (text: string, record: TableRecord) => {
378
                            const eachTrial = TRIALS.getTrial(record.id);
379
                            return (
380
                                <span>{eachTrial.description.parameters[realItem]}</span>
381
382
383
384
385
                            );
                        },
                    });
                    break;
                default:
386
387
                    // FIXME
                    alert('Unexpected column type');
Lijiao's avatar
Lijiao committed
388
            }
389
        }
Lijiao's avatar
Lijiao committed
390
391
392
393
394

        return (
            <Row className="tableList">
                <div id="tableList">
                    <Table
395
                        ref={(table: Table<TableRecord> | null) => this.tables = table}
396
                        columns={showColumn}
Lijiao's avatar
Lijiao committed
397
                        rowSelection={rowSelection}
Lijiao's avatar
Lijiao committed
398
                        expandedRowRender={this.openRow}
399
                        dataSource={tableSource}
Lijiao's avatar
Lijiao committed
400
                        className="commonTableStyle"
401
                        scroll={{x: 'max-content'}}
402
                        pagination={pageSize > 0 ? { pageSize } : false}
Lijiao's avatar
Lijiao committed
403
                    />
404
                    {/* Intermediate Result Modal */}
Lijiao's avatar
Lijiao committed
405
                    <Modal
Lijiao's avatar
Lijiao committed
406
                        title="Intermediate result"
Lijiao's avatar
Lijiao committed
407
408
409
410
411
412
                        visible={modalVisible}
                        onCancel={this.hideIntermediateModal}
                        footer={null}
                        destroyOnClose={true}
                        width="80%"
                    >
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
                        {
                            intermediateOtherKeys.length > 1
                                ?
                                <Row className="selectKeys">
                                    <Select
                                        className="select"
                                        defaultValue="default"
                                        onSelect={this.selectOtherKeys}
                                    >
                                        {
                                            Object.keys(intermediateOtherKeys).map(item => {
                                                const keys = intermediateOtherKeys[item];
                                                return <Option value={keys} key={item}>{keys}</Option>;
                                            })
                                        }
                                    </Select>

                                </Row>
                                :
                                <div />
                        }
Lijiao's avatar
Lijiao committed
434
435
436
437
438
439
440
441
442
443
                        <ReactEcharts
                            option={intermediateOption}
                            style={{
                                width: '100%',
                                height: 0.7 * window.innerHeight
                            }}
                            theme="my_theme"
                        />
                    </Modal>
                </div>
444
445
446
447
448
449
450
451
452
453
454
                {/* Add Column Modal */}
                <Modal
                    title="Table Title"
                    visible={isShowColumn}
                    onCancel={this.hideShowColumnModal}
                    footer={null}
                    destroyOnClose={true}
                    width="40%"
                >
                    <CheckboxGroup
                        options={showTitle}
455
456
                        defaultValue={columnList}
                        // defaultValue={columnSelected}
457
458
459
460
                        onChange={this.selectedColumn}
                        className="titleColumn"
                    />
                </Modal>
Lijiao's avatar
Lijiao committed
461
                <Compare compareRows={selectRows} visible={isShowCompareModal} cancelFunc={this.hideCompareModal} />
Lijiao's avatar
Lijiao committed
462
463
464
465
466
            </Row>
        );
    }
}

467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
const SequenceIdColumnConfig: ColumnProps<TableRecord> = {
    title: 'Trial No.',
    dataIndex: 'sequenceId',
    width: 120,
    className: 'tableHead',
    sorter: (a, b) => a.sequenceId - b.sequenceId
};

const IdColumnConfig: ColumnProps<TableRecord> = {
    title: 'ID',
    dataIndex: 'id',
    width: 60,
    className: 'tableHead leftTitle',
    sorter: (a, b) => a.id.localeCompare(b.id),
    render: (text, record) => (
        <div>{record.id}</div>
    )
};

const StartTimeColumnConfig: ColumnProps<TableRecord> = {
    title: 'Start Time',
    dataIndex: 'startTime',
    width: 160,
    render: (text, record) => (
        <span>{formatTimestamp(record.startTime)}</span>
    )
};

const EndTimeColumnConfig: ColumnProps<TableRecord> = {
    title: 'End Time',
    dataIndex: 'endTime',
    width: 160,
    render: (text, record) => (
        <span>{formatTimestamp(record.endTime, '--')}</span>
    )
};

const DurationColumnConfig: ColumnProps<TableRecord> = {
    title: 'Duration',
    dataIndex: 'duration',
    width: 100,
    sorter: (a, b) => a.duration - b.duration,
    render: (text, record) => (
        <div className="durationsty"><div>{convertDuration(record.duration)}</div></div>
    )
};

const StatusColumnConfig: ColumnProps<TableRecord> = {
    title: 'Status',
    dataIndex: 'status',
    width: 150,
    className: 'tableStatus',
    render: (text, record) => (
        <span className={`${record.status} commonStyle`}>{record.status}</span>
    ),
    sorter: (a, b) => a.status.localeCompare(b.status),
    filters: trialJobStatus.map(status => ({ text: status, value: status })),
    onFilter: (value, record) => (record.status === value)
};

const IntermediateCountColumnConfig: ColumnProps<TableRecord> = {
528
    title: 'Intermediate result',
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
    dataIndex: 'intermediateCount',
    width: 86,
    render: (text, record) => (
        <span>{`#${record.intermediateCount}`}</span>
    )
};

const AccuracyColumnConfig: ColumnProps<TableRecord> = {
    title: 'Default metric',
    className: 'leftTitle',
    dataIndex: 'accuracy',
    width: 120,
    sorter: (a, b, sortOrder) => {
        if (a.accuracy === undefined) {
            return sortOrder === 'ascend' ? -1 : 1;
        } else if (b.accuracy === undefined) {
            return sortOrder === 'ascend' ? 1 : -1;
        } else {
            return a.accuracy - b.accuracy;
        }
    },
    render: (text, record) => (
        // TODO: is this needed?
        <div>{record.latestAccuracy}</div>
    )
};

556
export default TableList;