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/dashboard/Data/Logs/Logs.react.js b/src/dashboard/Data/Logs/Logs.react.js index f78ea680bf..7adcb6e225 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,7 +33,10 @@ class Logs extends DashboardView { this.state = { logs: undefined, release: undefined, + loading: false, + hasMore: false, }; + this.latestLogsRequestId = 0; } componentDidMount() { @@ -47,14 +51,67 @@ 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; + } + const requestId = ++this.latestLogsRequestId; + this.setState({ loading: true }); + app.getLogs(typeParam, options).then( + newLogs => { + if (requestId !== this.latestLogsRequestId) { + return; + } + this.setState(prevState => { + let merged; + if (until && Array.isArray(prevState.logs)) { + const existingKeys = new Set( + prevState.logs.map(l => { + const ts = l.timestamp.iso || l.timestamp; + return `${ts}|${l.message}`; + }) + ); + const unique = newLogs.filter(l => { + const ts = l.timestamp.iso || l.timestamp; + return !existingKeys.has(`${ts}|${l.message}`); + }); + merged = prevState.logs.concat(unique); + } else { + merged = newLogs; + } + return { + logs: merged, + hasMore: newLogs.length > 0, + loading: false, + }; + }); + }, + () => { + if (requestId !== this.latestLogsRequestId) { + return; + } + this.setState(prevState => ({ + logs: prevState.logs || [], + hasMore: false, + loading: false, + })); + } ); } + handleLoadMore() { + const logs = this.state.logs; + if (!logs || logs.length === 0 || this.state.loading) { + 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 +172,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 }); }