Styles Account and APIKeys components

This commit is contained in:
Andrew Nicolaou 2019-05-15 12:11:58 +02:00 committed by Cassie Tarakajian
parent a03eed1603
commit 3e760ca0b8
8 changed files with 244 additions and 126 deletions

View file

@ -1,67 +1,19 @@
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';
const trashCan = require('../../../images/trash-can.svg');
import CopyableInput from '../../IDE/components/CopyableInput';
import APIKeyList from './APIKeyList';
function NewTokenDisplay({ token }) {
function handleCopyClick() {
navigator.clipboard.writeText(token);
}
const plusIcon = require('../../../images/plus-icon.svg');
return (
<React.Fragment>
<p>Make sure to copy your new personal access token now. You wont be able to see it again!</p>
<p><input type="text" readOnly value={token} /></p>
<button onClick={handleCopyClick}>Copy</button>
</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>
);
}
export const APIKeyPropType = PropTypes.shape({
id: PropTypes.object.isRequired,
label: PropTypes.string.isRequired,
createdAt: PropTypes.object.isRequired,
lastUsedAt: PropTypes.object.isRequired,
});
class APIKeyForm extends React.Component {
constructor(props) {
@ -99,45 +51,64 @@ class APIKeyForm extends React.Component {
if (hasApiKeys) {
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() {
const keyWithToken = this.props.apiKeys.find(k => !!k.token);
return (
<div>
<form id="addKeyForm" className="form" onSubmit={this.addKey}>
<label htmlFor="keyLabel" className="form__label">Name</label>
<input
type="text"
className="form__input"
placeholder="What is this token for?"
id="keyLabel"
onChange={(event) => { this.setState({ keyLabel: event.target.value }); }}
/>
<input
type="submit"
value="Create new Key"
disabled={this.state.keyLabel === ''}
/>
</form>
{this.renderApiKeys()}
<div className="api-key-form">
<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>
<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
className="form__input"
id="keyLabel"
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}
/>
<button
className="api-key-form__create-button"
disabled={this.state.keyLabel === ''}
type="submit"
>
<InlineSVG src={plusIcon} alt="" /> Create
</button>
</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 wont 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()}
</div>
</div>
);
}
}
APIKeyForm.propTypes = {
apiKeys: PropTypes.arrayOf(PropTypes.shape(APIKeyPropType)).isRequired,
createApiKey: 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;

View 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;

View file

@ -16,7 +16,6 @@ import APIKeyForm from '../components/APIKeyForm';
const exitUrl = require('../../../images/exit.svg');
const logoUrl = require('../../../images/p5js-logo.svg');
class AccountView extends React.Component {
constructor(props) {
super(props);
@ -38,7 +37,7 @@ class AccountView extends React.Component {
render() {
return (
<section className="form-container form-container--align-top">
<section className="form-container form-container--align-top form-container--align-left account-container">
<Helmet>
<title>p5.js Web Editor | Account</title>
</Helmet>
@ -52,7 +51,7 @@ class AccountView extends React.Component {
</div>
<div className="form-container__content">
<h2 className="form-container__title">My Account</h2>
<Tabs>
<Tabs className="account__tabs">
<TabList>
<div className="preference__subheadings">
<Tab><h4 className="preference__subheading">Account</h4></Tab>
@ -61,8 +60,9 @@ class AccountView extends React.Component {
</TabList>
<TabPanel>
<AccountForm {...this.props} />
<h2 className="form-container__divider">Or</h2>
<GithubButton buttonText="Login with Github" />
<h2 className="form-container__divider">Social Login</h2>
<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>
<APIKeyForm {...this.props} />

View file

@ -0,0 +1,4 @@
.account__tabs {
width: 500px;
padding-top: #{20 / $base-font-size}rem;
}

View 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;
}

View file

@ -6,6 +6,10 @@
align-items: center;
}
.form-container--align-left {
text-align: left;
}
.form-container--align-top {
height: unset;
}
@ -25,11 +29,21 @@
align-items: center;
}
.form-container--align-left .form-container__content {
align-items: left;
}
.form-container__title {
font-weight: normal;
color: $form-title-color;
}
.form-container__context {
@include themify() {
color: getThemifyVariable('secondary-text-color')
}
}
.form-container__divider {
padding: #{20 / $base-font-size}rem 0;
}
@ -41,20 +55,3 @@
.form-container__exit-button {
@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;
}
}

View file

@ -9,6 +9,11 @@
}
}
.form--inline {
display: flex;
align-items: center;
}
.form__cancel-button {
margin-top: #{10 / $base-font-size}rem;
font-size: #{12 / $base-font-size}rem;
@ -20,6 +25,11 @@
color: $form-navigation-options-color;
}
.form__legend{
font-size: #{21 / $base-font-size}rem;
font-weight: bold;
}
.form__label {
color: $secondary-form-title-color;
font-size: #{12 / $base-font-size}rem;
@ -28,6 +38,10 @@
display: block;
}
.form__label--hidden {
@extend %hidden-element;
}
.form__input {
width: #{360 / $base-font-size}rem;
height: #{40 / $base-font-size}rem;
@ -51,25 +65,3 @@
.form input[type="submit"]:disabled {
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;
}

View file

@ -14,6 +14,8 @@
@import 'components/p5-light-codemirror-theme';
@import 'components/p5-dark-codemirror-theme';
@import 'components/p5-contrast-codemirror-theme';
@import 'components/account';
@import 'components/api-key';
@import 'components/editor';
@import 'components/nav';
@import 'components/preview-nav';