App.tsx 9.91 KB
Newer Older
Deshui Yu's avatar
Deshui Yu committed
1
import * as React from 'react';
2
import { Stack } from '@fluentui/react';
3
4
import { COLUMN } from './static/const';
import { EXPERIMENT, TRIALS } from './static/datamodel';
5
import NavCon from './components/NavCon';
6
import MessageInfo from './components/modals/MessageInfo';
7
import { SlideNavBtns } from './components/slideNav/SlideNavBtns';
8
import './App.scss';
9
import './static/style/common.scss';
Deshui Yu's avatar
Deshui Yu committed
10

11
interface AppState {
12
    interval: number;
13
    columnList: string[];
14
15
    experimentUpdateBroadcast: number;
    trialsUpdateBroadcast: number;
16
    maxDurationUnit: string;
Lijiao's avatar
Lijiao committed
17
    metricGraphMode: 'max' | 'min'; // tuner's optimize_mode filed
18
19
    isillegalFinal: boolean;
    expWarningMessage: string;
Lijiaoa's avatar
Lijiaoa committed
20
    bestTrialEntries: string; // for overview page: best trial entreis
Lijiaoa's avatar
Lijiaoa committed
21
    isUpdate: boolean;
22
23
}

24
25
26
27
28
29
30
export const AppContext = React.createContext({
    interval: 10, // sendons
    columnList: COLUMN,
    experimentUpdateBroadcast: 0,
    trialsUpdateBroadcast: 0,
    metricGraphMode: 'max',
    bestTrialEntries: '10',
31
    maxDurationUnit: 'm',
32
33
34
    // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
    changeColumn: (val: string[]) => {},
    // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
35
    changeMetricGraphMode: (val: 'max' | 'min') => {},
36
    // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
37
38
    changeMaxDurationUnit: (val: string) => {},
    // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
39
40
41
    changeEntries: (val: string) => {},
    // eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
    updateOverviewPage: () => {}
42
43
});

44
class App extends React.Component<{}, AppState> {
45
46
    private timerId!: number | undefined;
    private firstLoad: boolean = false; // when click refresh selector options
47
48
49
50
51
52
53
    constructor(props: {}) {
        super(props);
        this.state = {
            interval: 10, // sendons
            columnList: COLUMN,
            experimentUpdateBroadcast: 0,
            trialsUpdateBroadcast: 0,
54
            metricGraphMode: 'max',
55
            maxDurationUnit: 'm',
56
            isillegalFinal: false,
Lijiaoa's avatar
Lijiaoa committed
57
            expWarningMessage: '',
Lijiaoa's avatar
Lijiaoa committed
58
59
            bestTrialEntries: '10',
            isUpdate: true
60
        };
61
62
    }

Lijiao's avatar
Lijiao committed
63
    async componentDidMount(): Promise<void> {
64
        await Promise.all([EXPERIMENT.init(), TRIALS.init()]);
65
66
        this.setState(state => ({
            experimentUpdateBroadcast: state.experimentUpdateBroadcast + 1,
Lijiaoa's avatar
Lijiaoa committed
67
            trialsUpdateBroadcast: state.trialsUpdateBroadcast + 1,
68
            metricGraphMode: EXPERIMENT.optimizeMode === 'minimize' ? 'min' : 'max'
Lijiaoa's avatar
Lijiaoa committed
69
70
        }));
        this.timerId = window.setTimeout(this.refresh, this.state.interval * 100);
71
72
    }

Lijiao's avatar
Lijiao committed
73
    changeInterval = (interval: number): void => {
74
75
        window.clearTimeout(this.timerId);
        if (interval === 0) {
Lijiaoa's avatar
Lijiaoa committed
76
            return;
77
        }
78
79
        // setState will trigger page refresh at once.
        // setState is asyc, interval not update to (this.state.interval) at once.
Lijiaoa's avatar
Lijiaoa committed
80
        this.setState({ interval }, () => {
81
82
83
            this.firstLoad = true;
            this.refresh();
        });
84
    };
85

86
    // TODO: use local storage
87
    changeColumn = (columnList: string[]): void => {
88
        this.setState({ columnList: columnList });
89
    };
90

Lijiao's avatar
Lijiao committed
91
    changeMetricGraphMode = (val: 'max' | 'min'): void => {
Lijiao's avatar
Lijiao committed
92
        this.setState({ metricGraphMode: val });
93
    };
Lijiao's avatar
Lijiao committed
94

Lijiaoa's avatar
Lijiaoa committed
95
96
    // overview best trial module
    changeEntries = (entries: string): void => {
Lijiaoa's avatar
Lijiaoa committed
97
        this.setState({ bestTrialEntries: entries });
98
    };
Lijiaoa's avatar
Lijiaoa committed
99

100
101
102
103
104
    // overview max duration unit
    changeMaxDurationUnit = (unit: string): void => {
        this.setState({ maxDurationUnit: unit });
    };

105
106
107
108
109
110
    updateOverviewPage = (): void => {
        this.setState(state => ({
            experimentUpdateBroadcast: state.experimentUpdateBroadcast + 1
        }));
    };

Lijiaoa's avatar
Lijiaoa committed
111
    shouldComponentUpdate(nextProps: any, nextState: AppState): boolean {
112
        if (!(nextState.isUpdate || nextState.isUpdate === undefined)) {
Lijiaoa's avatar
Lijiaoa committed
113
114
115
116
117
118
            nextState.isUpdate = true;
            return false;
        }
        return true;
    }

119
    render(): React.ReactNode {
120
121
122
123
124
125
126
127
        const {
            interval,
            columnList,
            experimentUpdateBroadcast,
            trialsUpdateBroadcast,
            metricGraphMode,
            isillegalFinal,
            expWarningMessage,
128
129
            bestTrialEntries,
            maxDurationUnit
130
        } = this.state;
131
        if (experimentUpdateBroadcast === 0 || trialsUpdateBroadcast === 0) {
132
            return null; // TODO: render a loading page
133
        }
Lijiaoa's avatar
Lijiaoa committed
134
135
136
137
138
139
140
141
        const errorList = [
            { errorWhere: TRIALS.jobListError(), errorMessage: TRIALS.getJobErrorMessage() },
            { errorWhere: EXPERIMENT.experimentError(), errorMessage: EXPERIMENT.getExperimentMessage() },
            { errorWhere: EXPERIMENT.statusError(), errorMessage: EXPERIMENT.getStatusMessage() },
            { errorWhere: TRIALS.MetricDataError(), errorMessage: TRIALS.getMetricDataErrorMessage() },
            { errorWhere: TRIALS.latestMetricDataError(), errorMessage: TRIALS.getLatestMetricDataErrorMessage() },
            { errorWhere: TRIALS.metricDataRangeError(), errorMessage: TRIALS.metricDataRangeErrorMessage() }
        ];
142
        return (
143
144
145
            <Stack className='nni' style={{ minHeight: window.innerHeight }}>
                <div className='header'>
                    <div className='headerCon'>
146
                        <NavCon changeInterval={this.changeInterval} refreshFunction={this.lastRefresh} />
147
148
                    </div>
                </div>
149
150
                <Stack className='contentBox'>
                    <Stack className='content'>
151
                        {/* search space & config */}
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
                        <AppContext.Provider
                            value={{
                                interval,
                                columnList,
                                changeColumn: this.changeColumn,
                                experimentUpdateBroadcast,
                                trialsUpdateBroadcast,
                                metricGraphMode,
                                maxDurationUnit,
                                changeMaxDurationUnit: this.changeMaxDurationUnit,
                                changeMetricGraphMode: this.changeMetricGraphMode,
                                bestTrialEntries,
                                changeEntries: this.changeEntries,
                                updateOverviewPage: this.updateOverviewPage
                            }}
                        >
168
                            <SlideNavBtns />
169
                        </AppContext.Provider>
Lijiaoa's avatar
Lijiaoa committed
170
                        {/* if api has error field, show error message */}
171
172
173
174
175
                        {errorList.map(
                            (item, key) =>
                                item.errorWhere && (
                                    <div key={key} className='warning'>
                                        <MessageInfo info={item.errorMessage} typeInfo='error' />
Lijiaoa's avatar
Lijiaoa committed
176
                                    </div>
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
                                )
                        )}
                        {isillegalFinal && (
                            <div className='warning'>
                                <MessageInfo info={expWarningMessage} typeInfo='warning' />
                            </div>
                        )}
                        <AppContext.Provider
                            value={{
                                interval,
                                columnList,
                                changeColumn: this.changeColumn,
                                experimentUpdateBroadcast,
                                trialsUpdateBroadcast,
                                metricGraphMode,
192
193
                                maxDurationUnit,
                                changeMaxDurationUnit: this.changeMaxDurationUnit,
194
195
                                changeMetricGraphMode: this.changeMetricGraphMode,
                                bestTrialEntries,
196
197
                                changeEntries: this.changeEntries,
                                updateOverviewPage: this.updateOverviewPage
198
199
                            }}
                        >
200
201
                            {this.props.children}
                        </AppContext.Provider>
202
203
204
                    </Stack>
                </Stack>
            </Stack>
205
206
207
        );
    }

Lijiao's avatar
Lijiao committed
208
    private refresh = async (): Promise<void> => {
209
210
211
212
213
214
215
216
217
218
219
220
        // resolve this question: 10s -> 20s, page refresh twice.
        // only refresh this page after clicking the refresh options
        if (this.firstLoad !== true) {
            const [experimentUpdated, trialsUpdated] = await Promise.all([EXPERIMENT.update(), TRIALS.update()]);
            if (experimentUpdated) {
                this.setState(state => ({ experimentUpdateBroadcast: state.experimentUpdateBroadcast + 1 }));
            }
            if (trialsUpdated) {
                this.setState(state => ({ trialsUpdateBroadcast: state.trialsUpdateBroadcast + 1 }));
            }
        } else {
            this.firstLoad = false;
221
222
        }

Lijiaoa's avatar
Lijiaoa committed
223
224
        // experiment status and /trial-jobs api's status could decide website update
        if (['DONE', 'ERROR', 'STOPPED'].includes(EXPERIMENT.status) || TRIALS.jobListError()) {
225
            // experiment finished, refresh once more to ensure consistency
Lijiaoa's avatar
Lijiaoa committed
226
            this.setState(() => ({ interval: 0, isUpdate: false }));
227
            return;
228
        }
229

Lijiaoa's avatar
Lijiaoa committed
230
        this.timerId = window.setTimeout(this.refresh, this.state.interval * 1000);
231
    };
232

233
    public async lastRefresh(): Promise<void> {
234
235
        await EXPERIMENT.update();
        await TRIALS.update(true);
236
237
238
239
        this.setState(state => ({
            experimentUpdateBroadcast: state.experimentUpdateBroadcast + 1,
            trialsUpdateBroadcast: state.trialsUpdateBroadcast + 1
        }));
240
    }
Deshui Yu's avatar
Deshui Yu committed
241
242
243
}

export default App;