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 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 won’t 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 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()}
|
||||
</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;
|
||||
|
|
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 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} />
|
||||
|
|
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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
Loading…
Reference in a new issue