From c8b8de6887fc2f8284b2d9680a759776f4746a4d Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 13 Mar 2026 21:24:24 +1100 Subject: [PATCH 1/5] chore: add .worktrees to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 5166e60131..00ad6a65be 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ test_logs # AI tools .claude +.worktrees From 695e4fbf0cfa75e5d808d01ff59fca955fbe3d29 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 13 Mar 2026 21:49:33 +1100 Subject: [PATCH 2/5] feat: Add log pagination with "Load more" button Closes #376 --- src/dashboard/Data/Logs/Logs.react.js | 45 ++++++++++++++++++++++++--- src/dashboard/Data/Logs/Logs.scss | 5 +++ src/lib/ParseApp.js | 25 ++++++++++----- 3 files changed, 63 insertions(+), 12 deletions(-) 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 }); } From f5f492139b01bdaeedeb845470b9d9de19de13ef Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 13 Mar 2026 21:14:05 +1100 Subject: [PATCH 3/5] feat: Add endpoint typeahead to API Console Closes #226 --- .../Autocomplete/Autocomplete.react.js | 13 ++- .../Data/ApiConsole/RestConsole.react.js | 88 ++++++++++++++++++- 2 files changed, 94 insertions(+), 7 deletions(-) diff --git a/src/components/Autocomplete/Autocomplete.react.js b/src/components/Autocomplete/Autocomplete.react.js index 705d78dab4..3373eb56d1 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(); @@ -343,13 +341,20 @@ 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 = { + ...suggestionsStyle, + ...(containerWidth ? { width: containerWidth + 'px' } : {}), + }; 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={() => { + if (!hasError) { + this.makeRequest(); + } + }} + buildSuggestions={input => this.buildEndpointSuggestions(input)} + buildLabel={() => ''} /> } /> From b587e689f2ec769abd312540f41511fedc4ba077 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 13 Mar 2026 22:02:28 +1100 Subject: [PATCH 4/5] fix: use keyboard-selected suggestion in Autocomplete onSubmit Resolve the active suggestion before calling onSubmit and onChange in onKeyDown, so Enter submits the highlighted item instead of stale typed text. Update RestConsole onSubmit to sync the provided endpoint to state before making the request. --- src/components/Autocomplete/Autocomplete.react.js | 11 ++++++++--- src/dashboard/Data/ApiConsole/RestConsole.react.js | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/components/Autocomplete/Autocomplete.react.js b/src/components/Autocomplete/Autocomplete.react.js index 3373eb56d1..c18022a922 100644 --- a/src/components/Autocomplete/Autocomplete.react.js +++ b/src/components/Autocomplete/Autocomplete.react.js @@ -250,15 +250,20 @@ 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 resolvedInput = filteredSuggestions[activeSuggestion] || 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, }); } else if (e.keyCode === 9) { // Tab diff --git a/src/dashboard/Data/ApiConsole/RestConsole.react.js b/src/dashboard/Data/ApiConsole/RestConsole.react.js index 43a9253a84..fd90610d19 100644 --- a/src/dashboard/Data/ApiConsole/RestConsole.react.js +++ b/src/dashboard/Data/ApiConsole/RestConsole.react.js @@ -290,9 +290,9 @@ export default class RestConsole extends Component { }} placeholder={'classes/_User'} onChange={endpoint => this.setState({ endpoint })} - onSubmit={() => { + onSubmit={(endpoint) => { if (!hasError) { - this.makeRequest(); + this.setState({ endpoint }, () => this.makeRequest()); } }} buildSuggestions={input => this.buildEndpointSuggestions(input)} From c7cb1b25c3ec810417bd8aadb089ed7d8882f898 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 14 Mar 2026 02:26:57 +1100 Subject: [PATCH 5/5] fix: respect strict mode and consumer-specified width in Autocomplete - Enter key handler now only accepts suggestion matches in strict mode, preventing raw user input from bypassing validation - Suggestions dropdown width from container measurement is now a fallback only, preserving explicit widths set via suggestionsStyle prop --- src/components/Autocomplete/Autocomplete.react.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/components/Autocomplete/Autocomplete.react.js b/src/components/Autocomplete/Autocomplete.react.js index c18022a922..015a07330a 100644 --- a/src/components/Autocomplete/Autocomplete.react.js +++ b/src/components/Autocomplete/Autocomplete.react.js @@ -250,7 +250,8 @@ export default class Autocomplete extends Component { const { userInput } = this.state; if (e.keyCode === 13 || e.key === 'Enter') { - const resolvedInput = filteredSuggestions[activeSuggestion] || 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); @@ -263,7 +264,7 @@ export default class Autocomplete extends Component { active: true, activeSuggestion: 0, showSuggestions: false, - userInput: resolvedInput, + userInput: resolvedInput || userInput, }); } else if (e.keyCode === 9) { // Tab @@ -350,8 +351,10 @@ export default class Autocomplete extends Component { ? this.fieldRef.current.offsetWidth : undefined; const mergedSuggestionsStyle = { + ...(containerWidth && !(suggestionsStyle && suggestionsStyle.width) + ? { width: containerWidth + 'px' } + : {}), ...suggestionsStyle, - ...(containerWidth ? { width: containerWidth + 'px' } : {}), }; suggestionsListComponent = (