diff --git a/.gitignore b/.gitignore index 5166e60131..00ad6a65be 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ test_logs # AI tools .claude +.worktrees diff --git a/src/components/Autocomplete/Autocomplete.react.js b/src/components/Autocomplete/Autocomplete.react.js index 705d78dab4..015a07330a 100644 --- a/src/components/Autocomplete/Autocomplete.react.js +++ b/src/components/Autocomplete/Autocomplete.react.js @@ -127,9 +127,7 @@ export default class Autocomplete extends Component { onClick(e) { const userInput = e.currentTarget.innerText; - if (this.props.strict) { - this.props.onChange && this.props.onChange(userInput); - } + this.props.onChange && this.props.onChange(userInput); const label = this.props.label || this.props.buildLabel(userInput); this.inputRef.current.focus(); @@ -252,15 +250,21 @@ export default class Autocomplete extends Component { const { userInput } = this.state; if (e.keyCode === 13 || e.key === 'Enter') { - if (userInput && userInput.length > 0 && this.props.onSubmit) { - this.props.onSubmit(userInput); + const suggestionMatch = filteredSuggestions[activeSuggestion]; + const resolvedInput = this.props.strict ? suggestionMatch : (suggestionMatch || userInput); + + if (resolvedInput && resolvedInput.length > 0) { + this.props.onChange && this.props.onChange(resolvedInput); + if (this.props.onSubmit) { + this.props.onSubmit(resolvedInput); + } } this.setState({ active: true, activeSuggestion: 0, showSuggestions: false, - userInput: filteredSuggestions[activeSuggestion] || userInput, + userInput: resolvedInput || userInput, }); } else if (e.keyCode === 9) { // Tab @@ -343,13 +347,22 @@ export default class Autocomplete extends Component { let suggestionsListComponent; if (showSuggestions && !hidden && filteredSuggestions.length) { + const containerWidth = this.fieldRef.current + ? this.fieldRef.current.offsetWidth + : undefined; + const mergedSuggestionsStyle = { + ...(containerWidth && !(suggestionsStyle && suggestionsStyle.width) + ? { width: containerWidth + 'px' } + : {}), + ...suggestionsStyle, + }; suggestionsListComponent = ( { + if (results) { + this.setState({ classNames: results.map(s => s.className) }); + } + }) + .catch(() => {}); + } + + buildEndpointSuggestions(input) { + const dynamicEndpoints = this.state.classNames.flatMap(className => [ + `classes/${className}`, + `schemas/${className}`, + `aggregate/${className}`, + `purge/${className}`, + ]); + + const allEndpoints = [...PARSE_API_ENDPOINTS, ...dynamicEndpoints]; + + if (!input) { + return allEndpoints; + } + + return allEndpoints.filter( + endpoint => endpoint.toLowerCase().indexOf(input.toLowerCase()) > -1 + ); + } + fetchUser() { if (this.state.runAsIdentifier.length === 0) { this.setState({ error: false, sessionToken: null }); @@ -210,11 +265,38 @@ export default class RestConsole extends Component { /> } input={ - this.setState({ endpoint })} + onSubmit={(endpoint) => { + if (!hasError) { + this.setState({ endpoint }, () => this.makeRequest()); + } + }} + buildSuggestions={input => this.buildEndpointSuggestions(input)} + buildLabel={() => ''} /> } /> diff --git a/src/dashboard/Data/Logs/Logs.react.js b/src/dashboard/Data/Logs/Logs.react.js index f78ea680bf..2683339542 100644 --- a/src/dashboard/Data/Logs/Logs.react.js +++ b/src/dashboard/Data/Logs/Logs.react.js @@ -5,6 +5,7 @@ * This source code is licensed under the license found in the LICENSE file in * the root directory of this source tree. */ +import Button from 'components/Button/Button.react'; import CategoryList from 'components/CategoryList/CategoryList.react'; import DashboardView from 'dashboard/DashboardView.react'; import EmptyState from 'components/EmptyState/EmptyState.react'; @@ -32,6 +33,8 @@ class Logs extends DashboardView { this.state = { logs: undefined, release: undefined, + loading: false, + hasMore: false, }; } @@ -47,14 +50,38 @@ class Logs extends DashboardView { } } - fetchLogs(app, type) { + fetchLogs(app, type, until) { + const PAGE_SIZE = 100; const typeParam = (type || 'INFO').toUpperCase(); - app.getLogs(typeParam).then( - logs => this.setState({ logs }), - () => this.setState({ logs: [] }) + const options = { size: PAGE_SIZE }; + if (until) { + options.until = until; + } + this.setState({ loading: true }); + app.getLogs(typeParam, options).then( + newLogs => { + this.setState(prevState => ({ + logs: until && Array.isArray(prevState.logs) + ? prevState.logs.concat(newLogs) + : newLogs, + hasMore: newLogs.length >= PAGE_SIZE, + loading: false, + })); + }, + () => this.setState({ logs: [], hasMore: false, loading: false }) ); } + handleLoadMore() { + const logs = this.state.logs; + if (!logs || logs.length === 0) { + return; + } + const oldestLog = logs[logs.length - 1]; + const oldestTimestamp = oldestLog.timestamp.iso || oldestLog.timestamp; + this.fetchLogs(this.context, this.props.params.type, oldestTimestamp); + } + // As parse-server doesn't support (yet?) versioning, we are disabling // this call in the meantime. @@ -115,6 +142,16 @@ class Logs extends DashboardView { ))} + {this.state.hasMore && ( +
+
+ )} ); } diff --git a/src/dashboard/Data/Logs/Logs.scss b/src/dashboard/Data/Logs/Logs.scss index acbba2298a..c8b0590048 100644 --- a/src/dashboard/Data/Logs/Logs.scss +++ b/src/dashboard/Data/Logs/Logs.scss @@ -25,3 +25,8 @@ right: 0; bottom: 0; } + +.showMore { + padding: 20px; + text-align: center; +} diff --git a/src/lib/ParseApp.js b/src/lib/ParseApp.js index 3da640d0ea..d992f9a793 100644 --- a/src/lib/ParseApp.js +++ b/src/lib/ParseApp.js @@ -144,15 +144,24 @@ export default class ParseApp { /** * Fetches scriptlogs from api.parse.com - * lines - maximum number of lines to fetch - * since - only fetch lines since this Date + * level - log level (info or error) + * options.from - only fetch logs after this date + * options.until - only fetch logs before this date + * options.size - maximum number of logs to fetch (default 100) + * options.order - sort order (asc or desc) */ - getLogs(level, since) { - const path = - 'scriptlog?level=' + - encodeURIComponent(level.toLowerCase()) + - '&n=100' + - (since ? '&startDate=' + encodeURIComponent(since.getTime()) : ''); + getLogs(level, { from, until, size = 100, order } = {}) { + let path = 'scriptlog?level=' + encodeURIComponent(level.toLowerCase()); + path += '&size=' + encodeURIComponent(size); + if (from) { + path += '&from=' + encodeURIComponent(from); + } + if (until) { + path += '&until=' + encodeURIComponent(until); + } + if (order) { + path += '&order=' + encodeURIComponent(order); + } return this.apiRequest('GET', path, {}, { useMasterKey: true }); }