Styles Account and APIKeys components
This commit is contained in:
parent
a03eed1603
commit
3e760ca0b8
8 changed files with 244 additions and 126 deletions
|
@ -1,67 +1,19 @@
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import InlineSVG from 'react-inlinesvg';
|
import InlineSVG from 'react-inlinesvg';
|
||||||
import format from 'date-fns/format';
|
|
||||||
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
|
|
||||||
import orderBy from 'lodash/orderBy';
|
|
||||||
|
|
||||||
const trashCan = require('../../../images/trash-can.svg');
|
import CopyableInput from '../../IDE/components/CopyableInput';
|
||||||
|
|
||||||
|
import APIKeyList from './APIKeyList';
|
||||||
|
|
||||||
function NewTokenDisplay({ token }) {
|
const plusIcon = require('../../../images/plus-icon.svg');
|
||||||
function handleCopyClick() {
|
|
||||||
navigator.clipboard.writeText(token);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
export const APIKeyPropType = PropTypes.shape({
|
||||||
<React.Fragment>
|
id: PropTypes.object.isRequired,
|
||||||
<p>Make sure to copy your new personal access token now. You won’t be able to see it again!</p>
|
label: PropTypes.string.isRequired,
|
||||||
<p><input type="text" readOnly value={token} /></p>
|
createdAt: PropTypes.object.isRequired,
|
||||||
<button onClick={handleCopyClick}>Copy</button>
|
lastUsedAt: PropTypes.object.isRequired,
|
||||||
</React.Fragment>
|
});
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function TokenMetadataList({ tokens, onRemove }) {
|
|
||||||
return (
|
|
||||||
<table className="form__table">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Name</th>
|
|
||||||
<th>Created on</th>
|
|
||||||
<th>Last used</th>
|
|
||||||
<th>Actions</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{orderBy(tokens, ['createdAt'], ['desc']).map((v) => {
|
|
||||||
const keyRow = (
|
|
||||||
<tr key={v.id}>
|
|
||||||
<td>{v.label}</td>
|
|
||||||
<td>{format(new Date(v.createdAt), 'MMM D, YYYY h:mm A')}</td>
|
|
||||||
<td>{distanceInWordsToNow(new Date(v.lastUsedAt), { addSuffix: true })}</td>
|
|
||||||
<td>
|
|
||||||
<button className="account__tokens__delete-button" onClick={() => onRemove(v)}>
|
|
||||||
<InlineSVG src={trashCan} alt="Delete Key" />
|
|
||||||
</button>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
|
|
||||||
const newKeyValue = v.token && (
|
|
||||||
<tr key={`${v.id}-newKey`}>
|
|
||||||
<td colSpan="4">
|
|
||||||
<NewTokenDisplay token={v.token} />
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
|
|
||||||
return [keyRow, newKeyValue];
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
class APIKeyForm extends React.Component {
|
class APIKeyForm extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
|
@ -99,45 +51,64 @@ class APIKeyForm extends React.Component {
|
||||||
|
|
||||||
if (hasApiKeys) {
|
if (hasApiKeys) {
|
||||||
return (
|
return (
|
||||||
<TokenMetadataList tokens={this.props.apiKeys} onRemove={this.removeKey} />
|
<APIKeyList apiKeys={this.props.apiKeys} onRemove={this.removeKey} />
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return <p>You have no API keys</p>;
|
return <p>You have no exsiting tokens.</p>;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const keyWithToken = this.props.apiKeys.find(k => !!k.token);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="api-key-form">
|
||||||
<form id="addKeyForm" className="form" onSubmit={this.addKey}>
|
<p className="api-key-form__summary">Personal Access Tokens act like your password to allow automated scripts to access the Editor API. Create a token for each script that needs access.</p>
|
||||||
<label htmlFor="keyLabel" className="form__label">Name</label>
|
|
||||||
|
<div className="api-key-form__section">
|
||||||
|
<h3 className="api-key-form__title">Create new token</h3>
|
||||||
|
<form className="form form--inline" onSubmit={this.addKey}>
|
||||||
|
<label htmlFor="keyLabel" className="form__label form__label--hidden ">What is this token for?</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
|
||||||
className="form__input"
|
className="form__input"
|
||||||
placeholder="What is this token for?"
|
|
||||||
id="keyLabel"
|
id="keyLabel"
|
||||||
onChange={(event) => { this.setState({ keyLabel: event.target.value }); }}
|
onChange={(event) => { this.setState({ keyLabel: event.target.value }); }}
|
||||||
|
placeholder="What is this token for? e.g. Example import script"
|
||||||
|
type="text"
|
||||||
|
value={this.state.keyLabel}
|
||||||
/>
|
/>
|
||||||
<input
|
<button
|
||||||
type="submit"
|
className="api-key-form__create-button"
|
||||||
value="Create new Key"
|
|
||||||
disabled={this.state.keyLabel === ''}
|
disabled={this.state.keyLabel === ''}
|
||||||
/>
|
type="submit"
|
||||||
|
>
|
||||||
|
<InlineSVG src={plusIcon} alt="" /> Create
|
||||||
|
</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
|
{
|
||||||
|
keyWithToken && (
|
||||||
|
<div className="api-key-form__new-token">
|
||||||
|
<h4 className="api-key-form__new-token__title">Your new access token</h4>
|
||||||
|
<p className="api-key-form__new-token__info">Make sure to copy your new personal access token now. You won’t be able to see it again!</p>
|
||||||
|
<CopyableInput label={keyWithToken.label} value={keyWithToken.token} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="api-key-form__section">
|
||||||
|
<h3 className="api-key-form__title">Existing tokens</h3>
|
||||||
{this.renderApiKeys()}
|
{this.renderApiKeys()}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
APIKeyForm.propTypes = {
|
APIKeyForm.propTypes = {
|
||||||
|
apiKeys: PropTypes.arrayOf(PropTypes.shape(APIKeyPropType)).isRequired,
|
||||||
createApiKey: PropTypes.func.isRequired,
|
createApiKey: PropTypes.func.isRequired,
|
||||||
removeApiKey: PropTypes.func.isRequired,
|
removeApiKey: PropTypes.func.isRequired,
|
||||||
apiKeys: PropTypes.arrayOf(PropTypes.shape({
|
|
||||||
id: PropTypes.object.isRequired,
|
|
||||||
label: PropTypes.string.isRequired,
|
|
||||||
createdAt: PropTypes.object.isRequired,
|
|
||||||
lastUsedAt: PropTypes.object.isRequired,
|
|
||||||
})).isRequired
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default APIKeyForm;
|
export default APIKeyForm;
|
||||||
|
|
50
client/modules/User/components/APIKeyList.jsx
Normal file
50
client/modules/User/components/APIKeyList.jsx
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import React from 'react';
|
||||||
|
import InlineSVG from 'react-inlinesvg';
|
||||||
|
import format from 'date-fns/format';
|
||||||
|
import distanceInWordsToNow from 'date-fns/distance_in_words_to_now';
|
||||||
|
import orderBy from 'lodash/orderBy';
|
||||||
|
|
||||||
|
import { APIKeyPropType } from './APIKeyForm';
|
||||||
|
|
||||||
|
const trashCan = require('../../../images/trash-can.svg');
|
||||||
|
|
||||||
|
function APIKeyList({ apiKeys, onRemove }) {
|
||||||
|
return (
|
||||||
|
<table className="api-key-list">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Created on</th>
|
||||||
|
<th>Last used</th>
|
||||||
|
<th>Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{orderBy(apiKeys, ['createdAt'], ['desc']).map((key) => {
|
||||||
|
const hasNewToken = !!key.token;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<tr key={key.id}>
|
||||||
|
<td>{key.label}</td>
|
||||||
|
<td>{format(new Date(key.createdAt), 'MMM D, YYYY h:mm A')}</td>
|
||||||
|
<td>{distanceInWordsToNow(new Date(key.lastUsedAt), { addSuffix: true })}</td>
|
||||||
|
<td className="api-key-list__action">
|
||||||
|
<button className="api-key-list__delete-button" onClick={() => onRemove(key)}>
|
||||||
|
<InlineSVG src={trashCan} alt="Delete Key" />
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
APIKeyList.propTypes = {
|
||||||
|
apiKeys: PropTypes.arrayOf(PropTypes.shape(APIKeyPropType)).isRequired,
|
||||||
|
onRemove: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default APIKeyList;
|
|
@ -16,7 +16,6 @@ import APIKeyForm from '../components/APIKeyForm';
|
||||||
const exitUrl = require('../../../images/exit.svg');
|
const exitUrl = require('../../../images/exit.svg');
|
||||||
const logoUrl = require('../../../images/p5js-logo.svg');
|
const logoUrl = require('../../../images/p5js-logo.svg');
|
||||||
|
|
||||||
|
|
||||||
class AccountView extends React.Component {
|
class AccountView extends React.Component {
|
||||||
constructor(props) {
|
constructor(props) {
|
||||||
super(props);
|
super(props);
|
||||||
|
@ -38,7 +37,7 @@ class AccountView extends React.Component {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<section className="form-container form-container--align-top">
|
<section className="form-container form-container--align-top form-container--align-left account-container">
|
||||||
<Helmet>
|
<Helmet>
|
||||||
<title>p5.js Web Editor | Account</title>
|
<title>p5.js Web Editor | Account</title>
|
||||||
</Helmet>
|
</Helmet>
|
||||||
|
@ -52,7 +51,7 @@ class AccountView extends React.Component {
|
||||||
</div>
|
</div>
|
||||||
<div className="form-container__content">
|
<div className="form-container__content">
|
||||||
<h2 className="form-container__title">My Account</h2>
|
<h2 className="form-container__title">My Account</h2>
|
||||||
<Tabs>
|
<Tabs className="account__tabs">
|
||||||
<TabList>
|
<TabList>
|
||||||
<div className="preference__subheadings">
|
<div className="preference__subheadings">
|
||||||
<Tab><h4 className="preference__subheading">Account</h4></Tab>
|
<Tab><h4 className="preference__subheading">Account</h4></Tab>
|
||||||
|
@ -61,8 +60,9 @@ class AccountView extends React.Component {
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<AccountForm {...this.props} />
|
<AccountForm {...this.props} />
|
||||||
<h2 className="form-container__divider">Or</h2>
|
<h2 className="form-container__divider">Social Login</h2>
|
||||||
<GithubButton buttonText="Login with Github" />
|
<p className="form-container__context">Link this account with your GitHub account to allow login from both.</p>
|
||||||
|
<GithubButton buttonText="Login with GitHub" />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<APIKeyForm {...this.props} />
|
<APIKeyForm {...this.props} />
|
||||||
|
|
4
client/styles/components/_account.scss
Normal file
4
client/styles/components/_account.scss
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
.account__tabs {
|
||||||
|
width: 500px;
|
||||||
|
padding-top: #{20 / $base-font-size}rem;
|
||||||
|
}
|
102
client/styles/components/_api-key.scss
Normal file
102
client/styles/components/_api-key.scss
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
.api-key-form__summary {
|
||||||
|
padding-top: #{25 / $base-font-size}rem;
|
||||||
|
@include themify() {
|
||||||
|
color: getThemifyVariable('heading-text-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-form__section {
|
||||||
|
padding-bottom: #{15 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-form__title {
|
||||||
|
padding: #{15 / $base-font-size}rem 0;
|
||||||
|
font-size: #{21 / $base-font-size}rem;
|
||||||
|
font-weight: bold;
|
||||||
|
@include themify() {
|
||||||
|
color: getThemifyVariable('heading-text-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-form__create-button {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-form__create-button .isvg {
|
||||||
|
padding-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-list {
|
||||||
|
display: block;
|
||||||
|
max-width: 900px;
|
||||||
|
border-collapse: collapse;
|
||||||
|
table-layout: fixed;
|
||||||
|
|
||||||
|
thead tr th {
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
|
||||||
|
thead tr th:last-child {
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
padding: #{5 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
padding: #{15 / $base-font-size}rem #{5 / $base-font-size}rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
tbody tr:nth-child(odd) {
|
||||||
|
background-color: #f2f2f2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-list__action {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-list__delete-button {
|
||||||
|
width:#{20 / $base-font-size}rem;
|
||||||
|
height:#{20 / $base-font-size}rem;
|
||||||
|
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
@include themify() {
|
||||||
|
background-color: transparent;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
position: initial;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
& g {
|
||||||
|
opacity: 1;
|
||||||
|
fill: getThemifyVariable('icon-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-list__delete-button:hover {
|
||||||
|
@include themify() {
|
||||||
|
& g {
|
||||||
|
opacity: 1;
|
||||||
|
fill: getThemifyVariable('icon-hover-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-form__new-token__title {
|
||||||
|
margin-bottom: #{10 / $base-font-size}rem;
|
||||||
|
font-size: #{18 / $base-font-size}rem;
|
||||||
|
font-weight: bold;
|
||||||
|
@include themify() {
|
||||||
|
color: getThemifyVariable('heading-text-color');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.api-key-form__new-token__info {
|
||||||
|
padding: #{10 / $base-font-size}rem 0;
|
||||||
|
}
|
|
@ -6,6 +6,10 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-container--align-left {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
.form-container--align-top {
|
.form-container--align-top {
|
||||||
height: unset;
|
height: unset;
|
||||||
}
|
}
|
||||||
|
@ -25,11 +29,21 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-container--align-left .form-container__content {
|
||||||
|
align-items: left;
|
||||||
|
}
|
||||||
|
|
||||||
.form-container__title {
|
.form-container__title {
|
||||||
font-weight: normal;
|
font-weight: normal;
|
||||||
color: $form-title-color;
|
color: $form-title-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form-container__context {
|
||||||
|
@include themify() {
|
||||||
|
color: getThemifyVariable('secondary-text-color')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.form-container__divider {
|
.form-container__divider {
|
||||||
padding: #{20 / $base-font-size}rem 0;
|
padding: #{20 / $base-font-size}rem 0;
|
||||||
}
|
}
|
||||||
|
@ -41,20 +55,3 @@
|
||||||
.form-container__exit-button {
|
.form-container__exit-button {
|
||||||
@extend %none-themify-icon-with-hover;
|
@extend %none-themify-icon-with-hover;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form__table {
|
|
||||||
display: block;
|
|
||||||
max-width: 900px;
|
|
||||||
border-collapse: collapse;
|
|
||||||
& td {
|
|
||||||
max-width: 300px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
padding: 0 10px 0 10px;
|
|
||||||
}
|
|
||||||
& tr:nth-child(even) {
|
|
||||||
background-color: #f2f2f2;
|
|
||||||
}
|
|
||||||
& tr.new-key {
|
|
||||||
background-color: #f2f2f2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -9,6 +9,11 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form--inline {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
.form__cancel-button {
|
.form__cancel-button {
|
||||||
margin-top: #{10 / $base-font-size}rem;
|
margin-top: #{10 / $base-font-size}rem;
|
||||||
font-size: #{12 / $base-font-size}rem;
|
font-size: #{12 / $base-font-size}rem;
|
||||||
|
@ -20,6 +25,11 @@
|
||||||
color: $form-navigation-options-color;
|
color: $form-navigation-options-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form__legend{
|
||||||
|
font-size: #{21 / $base-font-size}rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
.form__label {
|
.form__label {
|
||||||
color: $secondary-form-title-color;
|
color: $secondary-form-title-color;
|
||||||
font-size: #{12 / $base-font-size}rem;
|
font-size: #{12 / $base-font-size}rem;
|
||||||
|
@ -28,6 +38,10 @@
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form__label--hidden {
|
||||||
|
@extend %hidden-element;
|
||||||
|
}
|
||||||
|
|
||||||
.form__input {
|
.form__input {
|
||||||
width: #{360 / $base-font-size}rem;
|
width: #{360 / $base-font-size}rem;
|
||||||
height: #{40 / $base-font-size}rem;
|
height: #{40 / $base-font-size}rem;
|
||||||
|
@ -51,25 +65,3 @@
|
||||||
.form input[type="submit"]:disabled {
|
.form input[type="submit"]:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form__table-button-remove {
|
|
||||||
@extend %forms-button;
|
|
||||||
margin: 1rem 0 1rem 0;
|
|
||||||
@include themify() {
|
|
||||||
color: getThemifyVariable('console-error-background-color');
|
|
||||||
border-color: getThemifyVariable('console-error-background-color');
|
|
||||||
&:enabled:hover {
|
|
||||||
border-color: getThemifyVariable('console-error-background-color');
|
|
||||||
background-color: getThemifyVariable('console-error-background-color');
|
|
||||||
}
|
|
||||||
&:enabled:active {
|
|
||||||
border-color: getThemifyVariable('console-error-background-color');
|
|
||||||
background-color: getThemifyVariable('console-error-background-color');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.form__table-button-copy{
|
|
||||||
@extend %forms-button;
|
|
||||||
margin: 1rem 0 1rem 0;
|
|
||||||
}
|
|
||||||
|
|
|
@ -14,6 +14,8 @@
|
||||||
@import 'components/p5-light-codemirror-theme';
|
@import 'components/p5-light-codemirror-theme';
|
||||||
@import 'components/p5-dark-codemirror-theme';
|
@import 'components/p5-dark-codemirror-theme';
|
||||||
@import 'components/p5-contrast-codemirror-theme';
|
@import 'components/p5-contrast-codemirror-theme';
|
||||||
|
@import 'components/account';
|
||||||
|
@import 'components/api-key';
|
||||||
@import 'components/editor';
|
@import 'components/editor';
|
||||||
@import 'components/nav';
|
@import 'components/nav';
|
||||||
@import 'components/preview-nav';
|
@import 'components/preview-nav';
|
||||||
|
|
Loading…
Reference in a new issue