Split ApplicationsIndex into smaller parts

This commit is contained in:
SleepWalker 2018-11-04 07:31:31 +02:00
parent 4931ea5598
commit acbf61ab40
4 changed files with 372 additions and 255 deletions

View File

@ -6,7 +6,6 @@ import { FormattedMessage as Message } from 'react-intl';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { LinkButton } from 'components/ui/form'; import { LinkButton } from 'components/ui/form';
import { COLOR_GREEN, COLOR_BLUE } from 'components/ui'; import { COLOR_GREEN, COLOR_BLUE } from 'components/ui';
import { restoreScroll } from 'components/ui/scroll/scroll';
import { ContactLink } from 'components/contact'; import { ContactLink } from 'components/contact';
import styles from './applicationsIndex.scss'; import styles from './applicationsIndex.scss';
@ -14,127 +13,23 @@ import messages from './ApplicationsIndex.intl.json';
import cubeIcon from './icons/cube.svg'; import cubeIcon from './icons/cube.svg';
import loadingCubeIcon from './icons/loading-cube.svg'; import loadingCubeIcon from './icons/loading-cube.svg';
import toolsIcon from './icons/tools.svg'; import toolsIcon from './icons/tools.svg';
import ApplicationItem from './ApplicationItem'; import ApplicationsList from './list';
type Props = { type Props = {
clientId?: ?string, clientId?: ?string,
displayForGuest: bool, displayForGuest: bool,
applications: Array<OauthAppResponse>, applications: Array<OauthAppResponse>,
isLoading: bool, isLoading: bool,
deleteApp: (string) => Promise<any>, deleteApp: string => Promise<any>,
resetApp: (string, bool) => Promise<any>, resetApp: (string, bool) => Promise<any>
}; };
type State = { export default class ApplicationsIndex extends Component<Props> {
expandedApp: ?string,
};
export default class ApplicationsIndex extends Component<Props, State> {
state = {
expandedApp: null,
};
appsRefs: {[key: string]: ?HTMLDivElement} = {};
componentDidUpdate(prevProps: Props) {
const { applications, isLoading, clientId } = this.props;
if (isLoading !== prevProps.isLoading && applications.length) {
if (clientId && applications.some((app) => app.clientId === clientId)) {
requestAnimationFrame(() => this.onTileClick(clientId));
}
}
}
render() { render() {
const { displayForGuest, applications, isLoading, resetApp, deleteApp } = this.props;
const { expandedApp } = this.state;
let content: Node;
if (displayForGuest) {
content = (
<div className={styles.emptyState}>
<img src={toolsIcon} className={styles.emptyStateIcon} />
<div className={styles.emptyStateText}>
<div>
<Message {...messages.weDontKnowAnythingAboutYou}/>
</div>
<div>
<Message {...messages.youMustAuthToBegin}/>
</div>
</div>
<LinkButton
to="/login"
label={messages.authorization}
color={COLOR_BLUE}
className={styles.emptyStateActionButton}
/>
</div>
);
} else if (isLoading) {
content = (
<div className={styles.emptyState}>
<img src={loadingCubeIcon} className={styles.loadingStateIcon} />
</div>
);
} else if (applications.length > 0) {
content = (
<div>
<div className={styles.appsListTitleContainer}>
<div className={styles.appsListTitle}>
<Message {...messages.yourApplications} />
</div>
<LinkButton
to="/dev/applications/new"
label={messages.addNew}
color={COLOR_GREEN}
className={styles.appsListAddNewAppBtn}
/>
</div>
<div className={styles.appsListContainer}>
{applications.map((app: OauthAppResponse) => (
<div key={app.clientId} ref={(elem) => {this.appsRefs[app.clientId] = elem;}}>
<ApplicationItem
application={app}
expand={app.clientId === expandedApp}
onTileClick={this.onTileClick}
onResetSubmit={resetApp}
onDeleteSubmit={deleteApp}
/>
</div>
))}
</div>
</div>
);
} else {
content = (
<div className={styles.emptyState}>
<img src={cubeIcon} className={styles.emptyStateIcon} />
<div className={styles.emptyStateText}>
<div>
<Message {...messages.youDontHaveAnyApplication}/>
</div>
<div>
<Message {...messages.shallWeStart}/>
</div>
</div>
<LinkButton
to="/dev/applications/new"
label={messages.addNew}
color={COLOR_GREEN}
className={styles.emptyStateActionButton}
/>
</div>
);
}
return ( return (
<div className={styles.container}> <div className={styles.container}>
<div className={styles.welcomeContainer}> <div className={styles.welcomeContainer}>
<Message {...messages.accountsForDevelopers} > <Message {...messages.accountsForDevelopers}>
{(pageTitle: string) => ( {(pageTitle: string) => (
<h2 className={styles.welcomeTitle}> <h2 className={styles.welcomeTitle}>
<Helmet title={pageTitle} /> <Helmet title={pageTitle} />
@ -144,38 +39,120 @@ export default class ApplicationsIndex extends Component<Props, State> {
</Message> </Message>
<div className={styles.welcomeTitleDelimiter} /> <div className={styles.welcomeTitleDelimiter} />
<div className={styles.welcomeParagraph}> <div className={styles.welcomeParagraph}>
<Message {...messages.accountsAllowsYouYoUseOauth2} values={{ <Message
ourDocumentation: ( {...messages.accountsAllowsYouYoUseOauth2}
<a href="http://docs.ely.by/oauth.html" target="_blank"> values={{
<Message {...messages.ourDocumentation} /> ourDocumentation: (
</a> <a
), href="http://docs.ely.by/oauth.html"
}} /> target="_blank"
>
<Message
{...messages.ourDocumentation}
/>
</a>
)
}}
/>
</div> </div>
<div className={styles.welcomeParagraph}> <div className={styles.welcomeParagraph}>
<Message {...messages.ifYouHaveAnyTroubles} values={{ <Message
feedback: ( {...messages.ifYouHaveAnyTroubles}
<ContactLink> values={{
<Message {...messages.feedback} /> feedback: (
</ContactLink> <ContactLink>
), <Message {...messages.feedback} />
}} /> </ContactLink>
)
}}
/>
</div> </div>
</div> </div>
{content} {this.getContent()}
</div> </div>
); );
} }
onTileClick = (clientId: string) => { getContent() {
const expandedApp = this.state.expandedApp === clientId ? null : clientId; const {
displayForGuest,
applications,
isLoading,
resetApp,
deleteApp,
clientId
} = this.props;
this.setState({expandedApp}, () => { if (displayForGuest) {
if (expandedApp !== null) { return <Guest />;
// TODO: @SleepWalker: мб у тебя есть идея, как это сделать более правильно и менее дёргано? } else if (isLoading) {
setTimeout(() => restoreScroll(this.appsRefs[clientId]), 150); return <Loader />;
} } else if (applications.length > 0) {
}); return (
}; <ApplicationsList
applications={applications}
resetApp={resetApp}
deleteApp={deleteApp}
clientId={clientId}
/>
);
}
return <NoApps />;
}
}
function Loader() {
return (
<div className={styles.emptyState}>
<img src={loadingCubeIcon} className={styles.loadingStateIcon} />
</div>
);
}
function Guest() {
return (
<div className={styles.emptyState}>
<img src={toolsIcon} className={styles.emptyStateIcon} />
<div className={styles.emptyStateText}>
<div>
<Message {...messages.weDontKnowAnythingAboutYou} />
</div>
<div>
<Message {...messages.youMustAuthToBegin} />
</div>
</div>
<LinkButton
to="/login"
label={messages.authorization}
color={COLOR_BLUE}
className={styles.emptyStateActionButton}
/>
</div>
);
}
function NoApps() {
return (
<div className={styles.emptyState}>
<img src={cubeIcon} className={styles.emptyStateIcon} />
<div className={styles.emptyStateText}>
<div>
<Message {...messages.youDontHaveAnyApplication} />
</div>
<div>
<Message {...messages.shallWeStart} />
</div>
</div>
<LinkButton
to="/dev/applications/new"
label={messages.addNew}
color={COLOR_GREEN}
className={styles.emptyStateActionButton}
/>
</div>
);
} }

View File

@ -9,44 +9,162 @@ import { SKIN_LIGHT, COLOR_BLACK, COLOR_RED } from 'components/ui';
import { Input, Button } from 'components/ui/form'; import { Input, Button } from 'components/ui/form';
import Collapse from 'components/ui/collapse'; import Collapse from 'components/ui/collapse';
import styles from './applicationsIndex.scss'; import styles from '../applicationsIndex.scss';
import messages from './ApplicationsIndex.intl.json'; import messages from '../ApplicationsIndex.intl.json';
const ACTION_REVOKE_TOKENS = 'revoke-tokens'; const ACTION_REVOKE_TOKENS = 'revoke-tokens';
const ACTION_RESET_SECRET = 'reset-secret'; const ACTION_RESET_SECRET = 'reset-secret';
const ACTION_DELETE = 'delete'; const ACTION_DELETE = 'delete';
const actionButtons = [
{
type: ACTION_REVOKE_TOKENS,
label: messages.revokeAllTokens
},
{
type: ACTION_RESET_SECRET,
label: messages.resetClientSecret
},
{
type: ACTION_DELETE,
label: messages.delete
}
];
export default class ApplicationItem extends Component<{ export default class ApplicationItem extends Component<
application: OauthAppResponse, {
expand: bool, application: OauthAppResponse,
onTileClick: (string) => void, expand: bool,
onResetSubmit: (string, bool) => Promise<*>, onTileClick: string => void,
onDeleteSubmit: (string) => Promise<*>, onResetSubmit: (string, bool) => Promise<*>,
}, { onDeleteSubmit: string => Promise<*>
selectedAction: ?string, },
isActionPerforming: bool, {
detailsHeight: number, selectedAction: ?string,
}> { isActionPerforming: bool,
detailsHeight: number
}
> {
state = { state = {
selectedAction: null, selectedAction: null,
isActionPerforming: false, isActionPerforming: false,
detailsHeight: 0, detailsHeight: 0
}; };
render() { render() {
const { application: app, expand } = this.props; const { application: app, expand } = this.props;
const { selectedAction, isActionPerforming } = this.state; const { selectedAction } = this.state;
let actionContent: Node; return (
<div
className={classNames(styles.appItemContainer, {
[styles.appExpanded]: expand
})}
>
<div className={styles.appItemTile} onClick={this.onTileToggle}>
<div className={styles.appTileTitle}>
<div className={styles.appName}>{app.name}</div>
<div className={styles.appStats}>
Client ID: {app.clientId}
{typeof app.countUsers !== 'undefined' && (
<span>
{' | '}
<Message
{...messages.countUsers}
values={{
count: app.countUsers
}}
/>
</span>
)}
</div>
</div>
<div className={styles.appItemToggle}>
<div className={styles.appItemToggleIcon} />
</div>
</div>
<Collapse isOpened={expand} onRest={this.onCollapseRest}>
<div className={styles.appDetailsContainer}>
<div className={styles.appDetailsInfoField}>
<Link
to={`/dev/applications/${app.clientId}`}
className={styles.editAppLink}
>
<Message
{...messages.editDescription}
values={{
icon: (
<div
className={styles.pencilIcon}
/>
)
}}
/>
</Link>
<Input
label="Client ID:"
skin={SKIN_LIGHT}
disabled
value={app.clientId}
copy
/>
</div>
<div className={styles.appDetailsInfoField}>
<Input
label="Client Secret:"
skin={SKIN_LIGHT}
disabled
value={app.clientSecret}
copy
/>
</div>
<div className={styles.appDetailsDescription}>
<Message
{...messages.ifYouSuspectingThatSecretHasBeenCompromised}
/>
</div>
<div className={styles.appActionsButtons}>
{actionButtons.map(({ type, label }) => (
<Button
key={type}
label={label}
color={COLOR_BLACK}
className={styles.appActionButton}
disabled={
selectedAction
&& selectedAction !== type
}
onClick={this.onActionButtonClick(type)}
small
/>
))}
</div>
{this.getActionContent()}
</div>
</Collapse>
</div>
);
}
getActionContent() {
const { selectedAction, isActionPerforming } = this.state;
switch (selectedAction) { switch (selectedAction) {
case ACTION_REVOKE_TOKENS: case ACTION_REVOKE_TOKENS:
case ACTION_RESET_SECRET: case ACTION_RESET_SECRET:
actionContent = ( return (
<div> <div>
<div className={styles.appActionDescription}> <div className={styles.appActionDescription}>
<Message {...messages.allRefreshTokensWillBecomeInvalid} />{' '} <Message
<Message {...messages.takeCareAccessTokensInvalidation} /> {...messages.allRefreshTokensWillBecomeInvalid}
/>{' '}
<Message
{...messages.takeCareAccessTokensInvalidation}
/>
</div> </div>
<div className={styles.appActionsButtons}> <div className={styles.appActionsButtons}>
<Button <Button
@ -64,7 +182,10 @@ export default class ApplicationItem extends Component<{
) : ( ) : (
<div <div
className={styles.continueActionLink} className={styles.continueActionLink}
onClick={this.onResetSubmit(selectedAction === ACTION_RESET_SECRET)} onClick={this.onResetSubmit(
selectedAction
=== ACTION_RESET_SECRET
)}
> >
<Message {...messages.continue} /> <Message {...messages.continue} />
</div> </div>
@ -73,13 +194,17 @@ export default class ApplicationItem extends Component<{
</div> </div>
</div> </div>
); );
break;
case ACTION_DELETE: case ACTION_DELETE:
actionContent = ( return (
<div> <div>
<div className={styles.appActionDescription}> <div className={styles.appActionDescription}>
<Message {...messages.appAndAllTokenWillBeDeleted} />{' '} <Message
<Message {...messages.takeCareAccessTokensInvalidation} /> {...messages.appAndAllTokenWillBeDeleted}
/>{' '}
<Message
{...messages.takeCareAccessTokensInvalidation}
/>
</div> </div>
<div className={styles.appActionsButtons}> <div className={styles.appActionsButtons}>
<Button <Button
@ -107,100 +232,10 @@ export default class ApplicationItem extends Component<{
</div> </div>
</div> </div>
); );
break;
default: default:
actionContent = null; return null;
break;
} }
return (
<div className={classNames(styles.appItemContainer, {
[styles.appExpanded]: expand,
})}>
<div className={styles.appItemTile} onClick={this.onTileToggle}>
<div className={styles.appTileTitle}>
<div className={styles.appName}>
{app.name}
</div>
<div className={styles.appStats}>
Client ID: {app.clientId}
{typeof app.countUsers !== 'undefined' && (
<span>
{' | '}
<Message {...messages.countUsers} values={{
count: app.countUsers,
}} />
</span>
)}
</div>
</div>
<div className={styles.appItemToggle}>
<div className={styles.appItemToggleIcon} />
</div>
</div>
<Collapse isOpened={expand} onRest={this.onCollapseRest}>
<div className={styles.appDetailsContainer}>
<div className={styles.appDetailsInfoField}>
<Link to={`/dev/applications/${app.clientId}`} className={styles.editAppLink}>
<Message {...messages.editDescription} values={{
icon: <div className={styles.pencilIcon} />,
}} />
</Link>
<Input
label="Client ID:"
skin={SKIN_LIGHT}
disabled
value={app.clientId}
copy
/>
</div>
<div className={styles.appDetailsInfoField}>
<Input
label="Client Secret:"
skin={SKIN_LIGHT}
disabled
value={app.clientSecret}
copy
/>
</div>
<div className={styles.appDetailsDescription}>
<Message {...messages.ifYouSuspectingThatSecretHasBeenCompromised} />
</div>
<div className={styles.appActionsButtons}>
<Button
label={messages.revokeAllTokens}
color={COLOR_BLACK}
className={styles.appActionButton}
disabled={selectedAction && selectedAction !== ACTION_REVOKE_TOKENS}
onClick={this.onActionButtonClick(ACTION_REVOKE_TOKENS)}
small
/>
<Button
label={messages.resetClientSecret}
color={COLOR_BLACK}
className={styles.appActionButton}
disabled={selectedAction && selectedAction !== ACTION_RESET_SECRET}
onClick={this.onActionButtonClick(ACTION_RESET_SECRET)}
small
/>
<Button
label={messages.delete}
color={COLOR_BLACK}
className={styles.appActionButton}
disabled={selectedAction && selectedAction !== ACTION_DELETE}
onClick={this.onActionButtonClick(ACTION_DELETE)}
small
/>
</div>
{actionContent}
</div>
</Collapse>
</div>
);
} }
onTileToggle = () => { onTileToggle = () => {
@ -211,33 +246,33 @@ export default class ApplicationItem extends Component<{
onCollapseRest = () => { onCollapseRest = () => {
if (!this.props.expand && this.state.selectedAction) { if (!this.props.expand && this.state.selectedAction) {
this.setState({ this.setState({
selectedAction: null, selectedAction: null
}); });
} }
}; };
onActionButtonClick = (type: ?string) => () => { onActionButtonClick = (type: ?string) => () => {
this.setState({ this.setState({
selectedAction: type === this.state.selectedAction ? null : type, selectedAction: type === this.state.selectedAction ? null : type
}); });
}; };
onResetSubmit = (resetClientSecret: bool) => async () => { onResetSubmit = (resetClientSecret: bool) => async () => {
const { onResetSubmit, application } = this.props; const { onResetSubmit, application } = this.props;
this.setState({ this.setState({
isActionPerforming: true, isActionPerforming: true
}); });
await onResetSubmit(application.clientId, resetClientSecret); await onResetSubmit(application.clientId, resetClientSecret);
this.setState({ this.setState({
isActionPerforming: false, isActionPerforming: false,
selectedAction: null, selectedAction: null
}); });
}; };
onSubmitDelete = () => { onSubmitDelete = () => {
const { onDeleteSubmit, application } = this.props; const { onDeleteSubmit, application } = this.props;
this.setState({ this.setState({
isActionPerforming: true, isActionPerforming: true
}); });
onDeleteSubmit(application.clientId); onDeleteSubmit(application.clientId);
}; };

View File

@ -0,0 +1,102 @@
// @flow
import type { OauthAppResponse } from 'services/api/oauth';
import React from 'react';
import { restoreScroll } from 'components/ui/scroll/scroll';
import { FormattedMessage as Message } from 'react-intl';
import { LinkButton } from 'components/ui/form';
import { COLOR_GREEN } from 'components/ui';
import messages from '../ApplicationsIndex.intl.json';
import styles from '../applicationsIndex.scss';
import ApplicationItem from './ApplicationItem';
type Props = {
applications: Array<OauthAppResponse>,
deleteApp: string => Promise<any>,
resetApp: (string, bool) => Promise<any>,
clientId: ?string
};
type State = {
expandedApp: ?string
};
export default class ApplicationsList extends React.Component<Props, State> {
state = {
expandedApp: null
};
appsRefs: { [key: string]: ?HTMLDivElement } = {};
componentDidMount() {
this.checkForActiveApp();
}
componentDidUpdate() {
this.checkForActiveApp();
}
render() {
const { applications, resetApp, deleteApp } = this.props;
const { expandedApp } = this.state;
return (
<div>
<div className={styles.appsListTitleContainer}>
<div className={styles.appsListTitle}>
<Message {...messages.yourApplications} />
</div>
<LinkButton
to="/dev/applications/new"
label={messages.addNew}
color={COLOR_GREEN}
className={styles.appsListAddNewAppBtn}
/>
</div>
<div className={styles.appsListContainer}>
{applications.map((app) => (
<div
key={app.clientId}
ref={(elem) => {
this.appsRefs[app.clientId] = elem;
}}
>
<ApplicationItem
application={app}
expand={app.clientId === expandedApp}
onTileClick={this.onTileClick}
onResetSubmit={resetApp}
onDeleteSubmit={deleteApp}
/>
</div>
))}
</div>
</div>
);
}
checkForActiveApp() {
const { applications, clientId } = this.props;
const { expandedApp } = this.state;
if (
clientId
&& expandedApp !== clientId
&& applications.some((app) => app.clientId === clientId)
) {
requestAnimationFrame(() => this.onTileClick(clientId));
}
}
onTileClick = (clientId: string) => {
const expandedApp
= this.state.expandedApp === clientId ? null : clientId;
this.setState({ expandedApp }, () => {
if (expandedApp !== null) {
// TODO: @SleepWalker: мб у тебя есть идея, как это сделать более правильно и менее дёргано?
setTimeout(() => restoreScroll(this.appsRefs[clientId]), 150);
}
});
};
}

View File

@ -0,0 +1,3 @@
// @flow
export { default } from './ApplicationsList';