App.tsx 9.9 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 { TrialConfigButton } from './components/public-child/config/TrialConfigButton';
8
import './App.scss';
Deshui Yu's avatar
Deshui Yu committed
9

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

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

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

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

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

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

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

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

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

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

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

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

Lijiao's avatar
Lijiao committed
207
    private refresh = async (): Promise<void> => {
208
209
210
211
212
213
214
215
216
217
218
219
        // 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;
220
221
        }

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

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

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

export default App;