-
Notifications
You must be signed in to change notification settings - Fork 877
/
Indexation.js
413 lines (370 loc) · 10.3 KB
/
Indexation.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
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
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
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
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
/* global yoastIndexingData */
import { Component, flushSync, Fragment } from "@wordpress/element";
import { __ } from "@wordpress/i18n";
import { Alert, NewButton, ProgressBar } from "@yoast/components";
import { colors } from "@yoast/style-guide";
import PropTypes from "prop-types";
import { addHistoryState, removeSearchParam } from "../helpers/urlHelpers";
import IndexingError from "./IndexingError";
import RequestError from "../errors/RequestError";
import ParseError from "../errors/ParseError";
const STATE = {
/**
* When the process has not started yet, or has been stopped manually.
*/
IDLE: "idle",
/**
* When the indexing process is in progress.
*/
IN_PROGRESS: "in_progress",
/**
* When an error has occurred during the indexing process that has stopped the process.
*/
ERRORED: "errored",
/**
* When the indexing process has finished.
*/
COMPLETED: "completed",
};
/**
* Indexes the site and shows a progress bar indicating the indexing process' progress.
*/
class Indexation extends Component {
/**
* Indexing constructor.
*
* @param {Object} props The properties.
*/
constructor( props ) {
super( props );
this.settings = yoastIndexingData;
this.state = {
state: STATE.IDLE,
processed: 0,
error: null,
amount: parseInt( this.settings.amount, 10 ),
firstTime: (
this.settings.firstTime === "1"
),
};
this.startIndexing = this.startIndexing.bind( this );
this.stopIndexing = this.stopIndexing.bind( this );
}
/**
* Does an indexing request.
*
* @param {string} url The url of the indexing that should be done.
* @param {string} nonce The WordPress nonce value for in the header.
*
* @returns {Promise} The request promise.
*/
async doIndexingRequest( url, nonce ) {
const response = await fetch( url, {
method: "POST",
headers: {
"X-WP-Nonce": nonce,
},
} );
const responseText = await response.text();
let data;
try {
/*
* Sometimes, in case of a fatal error, or if WP_DEBUG is on and a DB query fails,
* non-JSON is dumped into the HTTP response body, so account for that here.
*/
data = JSON.parse( responseText );
} catch ( error ) {
throw new ParseError( "Error parsing the response to JSON.", responseText );
}
// Throw an error when the response's status code is not in the 200-299 range.
if ( ! response.ok ) {
const stackTrace = data.data ? data.data.stackTrace : "";
throw new RequestError( data.message, url, "POST", response.status, stackTrace );
}
return data;
}
/**
* Does any registered indexing action *before* a call to an index endpoint.
*
* @param {string} endpoint The endpoint that has been called.
*
* @returns {Promise<void>} An empty promise.
*/
async doPreIndexingAction( endpoint ) {
if ( typeof this.props.preIndexingActions[ endpoint ] === "function" ) {
await this.props.preIndexingActions[ endpoint ]( this.settings );
}
}
/**
* Does any registered indexing action *after* a call to an index endpoint.
*
* @param {string} endpoint The endpoint that has been called.
* @param {Object} response The response of the call to the endpoint.
*
* @returns {Promise<void>} An empty promise.
*/
async doPostIndexingAction( endpoint, response ) {
if ( typeof this.props.indexingActions[ endpoint ] === "function" ) {
await this.props.indexingActions[ endpoint ]( response.objects, this.settings );
}
}
/**
* Does the indexing of a given endpoint.
*
* @param {string} endpoint The endpoint.
*
* @returns {Promise} The indexing promise.
*/
async doIndexing( endpoint ) {
let url = this.settings.restApi.root + this.settings.restApi.indexing_endpoints[ endpoint ];
while ( this.isState( STATE.IN_PROGRESS ) && url !== false ) {
try {
await this.doPreIndexingAction( endpoint );
const response = await this.doIndexingRequest( url, this.settings.restApi.nonce );
await this.doPostIndexingAction( endpoint, response );
flushSync( () => {
this.setState( previousState => (
{
processed: previousState.processed + response.objects.length,
firstTime: false,
}
) );
} );
url = response.next_url;
} catch ( error ) {
flushSync( () => {
this.setState( {
state: STATE.ERRORED,
error: error,
firstTime: false,
} );
} );
url = false;
}
}
}
/**
* Indexes the objects by calling each indexing endpoint in turn.
*
* @returns {Promise<void>} The indexing promise.
*/
async index() {
for ( const endpoint of Object.keys( this.settings.restApi.indexing_endpoints ) ) {
await this.doIndexing( endpoint );
}
// Wait until the next cycle, to make sure the state is updated before checking it.
await new Promise( resolve => setTimeout( resolve, 0 ) );
/*
* Set the indexing process as completed only when there is no error
* and the user has not stopped the process manually.
*/
if ( ! this.isState( STATE.ERRORED ) && ! this.isState( STATE.IDLE ) ) {
this.completeIndexing();
}
}
/**
* Starts the indexing process.
*
* @returns {Promise<void>} The start indexing promise.
*/
async startIndexing() {
/*
* Since `setState` is asynchronous in nature, we have to supply a callback
* to make sure the state is correctly set before trying to call the first
* endpoint.
*/
this.setState( { processed: 0, state: STATE.IN_PROGRESS }, this.index );
}
/**
* Sets the state of the indexing process to completed.
*
* @returns {void}
*/
completeIndexing() {
this.setState( { state: STATE.COMPLETED } );
}
/**
* Stops the indexing process.
*
* @returns {void}
*/
stopIndexing() {
this.setState( previousState => (
{
state: STATE.IDLE,
processed: 0,
amount: previousState.amount - previousState.processed,
}
) );
}
/**
* Start indexation on mount, when redirected from the "Start SEO data optimization" button in the dashboard notification.
*
* @returns {void}
*/
componentDidMount() {
if ( this.settings.disabled ) {
return;
}
this.props.indexingStateCallback( this.state.amount === 0 ? "completed" : this.state.state );
const shouldStart = new URLSearchParams( window.location.search ).get( "start-indexation" ) === "true";
if ( shouldStart ) {
const currentURL = removeSearchParam( window.location.href, "start-indexation" );
addHistoryState( null, document.title, currentURL );
this.startIndexing();
}
}
/**
* Signals state changes to an optional callback function.
*
* @param {Object} _prevProps The previous props, unused in the current implementation.
* @param {Object} prevState The previous state.
*
* @returns {void}
*/
componentDidUpdate( _prevProps, prevState ) {
if ( this.state.state !== prevState.state ) {
this.props.indexingStateCallback( this.state.state );
}
}
/**
* If the current state of the indexing process is the given state.
*
* @param {STATE.IDLE|STATE.ERRORED|STATE.IN_PROGRESS|STATE.COMPLETED} state The state value to check against.
*
* @returns {boolean} If the current state of the indexing process is the given state.
*/
isState( state ) {
return this.state.state === state;
}
/**
* Renders a notice if it is the first time the indexation is performed.
*
* @returns {JSX.Element} The rendered component.
*/
renderFirstIndexationNotice() {
return (
<Alert type={ "info" }>
{ __( "This feature includes and replaces the Text Link Counter and Internal Linking Analysis", "wordpress-seo" ) }
</Alert>
);
}
/**
* Renders the start button.
*
* @returns {JSX.Element|null} The start button.
*/
renderStartButton() {
return <NewButton
variant="primary"
onClick={ this.startIndexing }
>
{ __( "Start SEO data optimization", "wordpress-seo" ) }
</NewButton>;
}
/**
* Renders the stop button.
*
* @returns {JSX.Element|null} The stop button.
*/
renderStopButton() {
return <NewButton
variant="secondary"
onClick={ this.stopIndexing }
>
{ __( "Stop SEO data optimization", "wordpress-seo" ) }
</NewButton>;
}
/**
* Renders the disabled tool.
*
* @returns {JSX.Element} The disabled tool.
*/
renderDisabledTool() {
return <Fragment>
<p>
<NewButton
variant="secondary"
disabled={ true }
>
{ __( "Start SEO data optimization", "wordpress-seo" ) }
</NewButton>
</p>
<Alert type={ "info" }>
{ __( "SEO data optimization is disabled for non-production environments.", "wordpress-seo" ) }
</Alert>
</Fragment>;
}
/**
* Renders the progress bar, plus caption.
*
* @returns {JSX.Element} The progress bar, plus caption.
*/
renderProgressBar() {
return <Fragment>
<ProgressBar
style={ { height: "16px", margin: "8px 0" } }
progressColor={ colors.$color_pink_dark }
max={ parseInt( this.state.amount, 10 ) }
value={ this.state.processed }
/>
<p style={ { color: colors.$palette_grey_text } }>
{ __( "Optimizing SEO data... This may take a while.", "wordpress-seo" ) }
</p>
</Fragment>;
}
/**
* Renders the error alert.
*
* @returns {JSX.Element} The error alert.
*/
renderErrorAlert() {
return <IndexingError
message={ yoastIndexingData.errorMessage }
error={ this.state.error }
/>;
}
/**
* Renders the indexing tool.
*
* @returns {JSX.Element} The indexing tool.
*/
renderTool() {
return (
<Fragment>
{ this.isState( STATE.IN_PROGRESS ) && this.renderProgressBar() }
{ this.isState( STATE.ERRORED ) && this.renderErrorAlert() }
{ this.isState( STATE.IDLE ) && this.state.firstTime && this.renderFirstIndexationNotice() }
{ this.isState( STATE.IN_PROGRESS )
? this.renderStopButton()
: this.renderStartButton()
}
</Fragment>
);
}
/**
* Renders the component
*
* @returns {JSX.Element} The rendered component.
*/
render() {
if ( this.settings.disabled ) {
return this.renderDisabledTool();
}
if ( this.isState( STATE.COMPLETED ) || this.state.amount === 0 ) {
return <Alert type={ "success" }>{ __( "SEO data optimization complete", "wordpress-seo" ) }</Alert>;
}
return this.renderTool();
}
}
Indexation.propTypes = {
indexingActions: PropTypes.object,
preIndexingActions: PropTypes.object,
indexingStateCallback: PropTypes.func,
};
Indexation.defaultProps = {
indexingActions: {},
preIndexingActions: {},
indexingStateCallback: () => {},
};
export default Indexation;