Skip to content

Commit

Permalink
Merge branch 'master' into feature/react-refactor-1
Browse files Browse the repository at this point in the history
  • Loading branch information
robertlong committed Jun 1, 2020
2 parents c2f037f + e04f0c8 commit bccdf86
Show file tree
Hide file tree
Showing 62 changed files with 2,936 additions and 1,330 deletions.
2 changes: 1 addition & 1 deletion admin/src/admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const theme = createMuiTheme({
MuiDrawer: {
docked: {
background: "#222222",
height: "100vh"
minHeight: "100vh"
}
}
},
Expand Down
275 changes: 213 additions & 62 deletions admin/src/react-components/accounts.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import React, { Component } from "react";
import { connect } from "react-redux";
import { IdentityEditLink, IdentityCreateLink } from "./fields";
import { withStyles } from "@material-ui/core/styles";
import Card from "@material-ui/core/Card";
Expand All @@ -23,7 +24,8 @@ import {
SelectInput,
SimpleForm,
TextField,
TextInput
TextInput,
refreshView
} from "react-admin";

const styles = {
Expand All @@ -39,70 +41,219 @@ const AccountFilter = props => (
);

export const AccountList = withStyles(styles)(
class AccountList extends Component {
state = {};
async onAccountSearch(e) {
e.preventDefault();
this.setState({ searching: true, accountSearchStatus: null });
const result = await fetch("/api/v1/accounts/search", {
method: "post",
headers: {
"content-type": "application/json",
authorization: `bearer ${window.APP.store.state.credentials.token}`
},
body: JSON.stringify({ email: this.state.email || "" })
}).then(r => r.json());
if (result && result.data) {
window.location = `#/accounts/${result.data[0].id}`;
} else {
this.setState({ searching: false, accountSearchStatus: "Account not found" });
connect(
undefined,
{ refreshView }
)(
class AccountList extends Component {
state = {
emailSearch: "",
searching: false,
searchStatus: null,
batchCreate: "",
creating: false,
createStatus: null,
createResults: ""
};
componentWillUnmount() {
this.clearCreateStatusTimer();
this.clearSearchStatusTimer();
}
}
render() {
const { classes } = this.props;

return (
<>
<Card classes={{ root: classes.searchCard }}>
<CardContent>
<Typography component="h2">Find an account with an email address</Typography>
<form onSubmit={this.onAccountSearch.bind(this)}>
<MuiTextField
label="Account Email"
type="email"
required
onChange={e => this.setState({ email: e.target.value })}
/>
<Button onClick={this.onAccountSearch.bind(this)}>Find</Button>
{this.state.searching && <CircularProgress />}
<Snackbar open={!!this.state.accountSearchStatus} autoHideDuration={5000}>
<SnackbarContent message={this.state.accountSearchStatus}></SnackbarContent>
</Snackbar>
</form>
</CardContent>
</Card>
<List {...this.props} filters={<AccountFilter />}>
<Datagrid>
<TextField source="id" />
<DateField source="inserted_at" />
<DateField source="updated_at" />
<ReferenceManyField label="Identity" target="_account_id" reference="identities">
<Datagrid classes={{ rowCell: classes.noBorder, thead: classes.hide }}>
<TextField source="name" />
<IdentityEditLink />
</Datagrid>
</ReferenceManyField>
clearCreateStatusTimer() {
if (this.createStatusTimer) {
clearTimeout(this.createStatusTimer);
this.createStatusTimer = null;
}
}
clearSearchStatusTimer() {
if (this.searchStatusTimer) {
clearTimeout(this.searchStatusTimer);
this.searchStatusTimer = null;
}
}
async onAccountSearch(e) {
e.preventDefault();
this.setState({ searching: true, searchStatus: null });
const result = await fetch("/api/v1/accounts/search", {
method: "post",
headers: {
"content-type": "application/json",
authorization: `bearer ${window.APP.store.state.credentials.token}`
},
body: JSON.stringify({ email: this.state.emailSearch || "" })
}).then(r => r.json());
if (result && result.data) {
window.location = `#/accounts/${result.data[0].id}`;
} else {
this.setState({ searching: false, searchStatus: "Account not found" });
}
// Quickfix snackbar component does not always close
// Setting snackbar message to empty string closes
this.clearSearchStatusTimer();
this.searchStatusTimer = setTimeout(() => {
this.setState({ searchStatus: "" });
this.searchStatusTimer = null;
}, 6000);
}
async onCreateAccount(e) {
e.preventDefault();
if (this.state.batchCreate.length === 0) return;
this.setState({ creating: true, createStatus: null });
const data = this.state.batchCreate
.split(";") // ['email1,identity1', '', 'email2','email3,identity with spaces', 'email4']
.filter(accounts => accounts !== "")
.map(accounts => {
const emailAndIdentity = accounts.split(",");
return emailAndIdentity.length === 1
? {
email: emailAndIdentity[0].trim()
}
: {
email: emailAndIdentity[0].trim(),
name: emailAndIdentity[1].trim()
};
});
const result = await fetch("/api/v1/accounts", {
method: "post",
headers: {
"content-type": "application/json",
authorization: `bearer ${window.APP.store.state.credentials.token}`
},
body: JSON.stringify({
data: data.length === 1 ? data[0] : data
})
}).then(r => r.json());
if (result && result.data) {
// one email added successfully
this.setState({ creating: false, createStatus: `Account created successfully` });
} else if (result && result.errors) {
// one email has errors
this.setState({ creating: false, createStatus: result.errors[0].detail });
} else if (Array.isArray(result)) {
// Multiple email accounts created
// results = {
// 'successMsg': [email1, ..., email3],
// 'errorMsg1': [email4],
// 'errorMsg2': [email5, email6]
// }
const results = {};
let isAllSuccess = true;
let hasOneSuccess = false;
result.forEach((emailResponse, index) => {
isAllSuccess = isAllSuccess && emailResponse.status === 200;
hasOneSuccess = hasOneSuccess || emailResponse.status === 200;
const message =
emailResponse.status === 200 ? "Created accounts successfully" : emailResponse.body.errors[0].detail;
const email = data[index].email;
if (results[message]) results[message].push(email);
else results[message] = [email];
});
this.setState({
creating: false,
createStatus: isAllSuccess
? "Success adding all accounts"
: hasOneSuccess
? "Success adding some accounts, Errors adding some accounts"
: "Errors adding all accounts",
createResults: results
});
}
this.props.refreshView();
// Quickfix snackbar component does not always close
// Setting snackbar message to empty string closes
this.clearCreateStatusTimer();
this.createStatusTimer = setTimeout(() => {
this.setState({ createStatus: "" });
this.createStatusTimer = null;
}, 6000);
}
render() {
const { classes } = this.props;
return (
<>
<Card classes={{ root: classes.searchCard }}>
<CardContent>
<Typography component="h2">
<b>Create one or multiple accounts with (optional) identities</b>
</Typography>
<Typography component="h3">
<i>Single example:</i> email1,identity1
</Typography>
<Typography component="h3">
<i>Multiple example:</i> email1,identity1;email2;email3,identity3 with spaces;email4
</Typography>
<form onSubmit={this.onCreateAccount.bind(this)}>
<MuiTextField
label="Email, (optional) identity"
type="text"
style={{ minWidth: "300px" }}
required
onChange={e => this.setState({ batchCreate: e.target.value })}
/>
<Button onClick={this.onCreateAccount.bind(this)}>Create</Button>
{this.state.creating && <CircularProgress />}
<Snackbar open={this.state.createStatus} autoHideDuration={5000}>
<SnackbarContent message={this.state.createStatus}></SnackbarContent>
</Snackbar>
</form>
{this.state.createResults &&
Object.keys(this.state.createResults).map(message => (
<>
<Typography
component="p"
color={message.includes("success") ? "textPrimary" : "error"}
style={{ paddingTop: "10px" }}
>
{message}
</Typography>
<Typography component="p" color="secondary" style={{ paddingBottom: "10px" }}>
[ {this.state.createResults[message].join(", ")} ]
</Typography>
</>
))}
</CardContent>
</Card>
<Card classes={{ root: classes.searchCard }}>
<CardContent>
<Typography component="h2">Find an account with an email address</Typography>
<form onSubmit={this.onAccountSearch.bind(this)}>
<MuiTextField
label="Account Email"
type="email"
required
onChange={e => this.setState({ emailSearch: e.target.value })}
/>
<Button onClick={this.onAccountSearch.bind(this)}>Find</Button>
{this.state.searching && <CircularProgress />}
<Snackbar open={this.state.searchStatus} autoHideDuration={5000}>
<SnackbarContent message={this.state.searchStatus}></SnackbarContent>
</Snackbar>
</form>
</CardContent>
</Card>
<List {...this.props} filters={<AccountFilter />}>
<Datagrid>
<TextField source="id" />
<DateField source="inserted_at" />
<DateField source="updated_at" />
<ReferenceManyField label="Identity" target="_account_id" reference="identities">
<Datagrid classes={{ rowCell: classes.noBorder, thead: classes.hide }}>
<TextField source="name" />
<IdentityEditLink />
</Datagrid>
</ReferenceManyField>

<IdentityCreateLink />
<BooleanField source="is_admin" />
<TextField source="state" />
<EditButton />
</Datagrid>
</List>
</>
);
<IdentityCreateLink />
<BooleanField source="is_admin" />
<TextField source="state" />
<EditButton />
</Datagrid>
</List>
</>
);
}
}
}
)
);

export const AccountEdit = withStyles(styles)(props => {
Expand Down
2 changes: 1 addition & 1 deletion habitat/plan.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ pkg_build_deps=(
)

pkg_deps=(
core/aws-cli # AWS cli used for run hook when uploading to S3
core/aws-cli/1.16.118/20190305224525 # AWS cli used for run hook when uploading to S3
)

do_build() {
Expand Down
Loading

0 comments on commit bccdf86

Please sign in to comment.