Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,4 @@ test_logs

# AI tools
.claude
.worktrees
27 changes: 20 additions & 7 deletions src/components/Autocomplete/Autocomplete.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = (
<SuggestionsList
position={this.state.position}
ref={this.dropdownRef}
onExternalClick={onExternalClick}
suggestions={filteredSuggestions}
suggestionsStyle={suggestionsStyle}
suggestionsStyle={mergedSuggestionsStyle}
suggestionsItemStyle={suggestionsItemStyle}
activeSuggestion={activeSuggestion}
onClick={onClick}
Expand Down
88 changes: 85 additions & 3 deletions src/dashboard/Data/ApiConsole/RestConsole.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 Autocomplete from 'components/Autocomplete/Autocomplete.react';
import Button from 'components/Button/Button.react';
import Dropdown from 'components/Dropdown/Dropdown.react';
import Field from 'components/Field/Field.react';
Expand All @@ -26,6 +27,29 @@ import Toggle from 'components/Toggle/Toggle.react';
import Toolbar from 'components/Toolbar/Toolbar.react';
import { CurrentApp } from 'context/currentApp';

const PARSE_API_ENDPOINTS = [
'batch',
'classes/',
'users',
'login',
'logout',
'sessions',
'roles',
'files/',
'events/',
'push',
'installations',
'functions/',
'jobs/',
'schemas/',
'config',
'hooks/functions',
'hooks/triggers',
'aggregate/',
'purge/',
'health',
];

export default class RestConsole extends Component {
static contextType = CurrentApp;
constructor() {
Expand All @@ -43,9 +67,40 @@ export default class RestConsole extends Component {
inProgress: false,
error: false,
curlModal: false,
classNames: [],
};
}

componentDidMount() {
this.context
.apiRequest('GET', 'schemas', {}, { useMasterKey: true })
.then(({ results }) => {
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 });
Expand Down Expand Up @@ -210,11 +265,38 @@ export default class RestConsole extends Component {
/>
}
input={
<TextInput
value={this.state.endpoint}
monospace={true}
<Autocomplete
inputStyle={{
width: '100%',
height: '80px',
textAlign: 'center',
border: 'none',
fontSize: '16px',
fontFamily: '"Source Code Pro", "Courier New", monospace',
background: 'transparent',
padding: '0 6px',
outline: 'none',
}}
containerStyle={{ width: '100%', height: 'auto' }}
suggestionsStyle={{
maxHeight: '200px',
overflowY: 'auto',
fontFamily: '"Source Code Pro", "Courier New", monospace',
fontSize: '14px',
borderRadius: '0 0 5px 5px',
}}
suggestionsItemStyle={{
padding: '8px 12px',
}}
placeholder={'classes/_User'}
onChange={endpoint => this.setState({ endpoint })}
onSubmit={(endpoint) => {
if (!hasError) {
this.setState({ endpoint }, () => this.makeRequest());
}
}}
buildSuggestions={input => this.buildEndpointSuggestions(input)}
buildLabel={() => ''}
/>
}
/>
Expand Down
45 changes: 41 additions & 4 deletions src/dashboard/Data/Logs/Logs.react.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -32,6 +33,8 @@ class Logs extends DashboardView {
this.state = {
logs: undefined,
release: undefined,
loading: false,
hasMore: false,
};
}

Expand All @@ -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.

Expand Down Expand Up @@ -115,6 +142,16 @@ class Logs extends DashboardView {
<LogViewEntry key={timestamp} text={message} timestamp={timestamp} />
))}
</LogView>
{this.state.hasMore && (
<div className={styles.showMore}>
<Button
progress={this.state.loading}
color="blue"
value="Load more logs"
onClick={() => this.handleLoadMore()}
/>
</div>
)}
</div>
);
}
Expand Down
5 changes: 5 additions & 0 deletions src/dashboard/Data/Logs/Logs.scss
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,8 @@
right: 0;
bottom: 0;
}

.showMore {
padding: 20px;
text-align: center;
}
25 changes: 17 additions & 8 deletions src/lib/ParseApp.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
}

Expand Down
Loading