Implemented desktop application type

This commit is contained in:
ErickSkrauch
2025-01-15 15:48:43 +01:00
parent beb3927f78
commit 91145abcf7
13 changed files with 359 additions and 222 deletions

View File

@@ -3,7 +3,7 @@ import { storiesOf } from '@storybook/react';
import { action } from '@storybook/addon-actions';
import ApplicationsIndex from './ApplicationsIndex';
import { TYPE_APPLICATION } from 'app/components/dev/apps';
import { TYPE_WEB_APPLICATION } from 'app/components/dev/apps';
import { OauthAppResponse } from 'app/services/api/oauth';
import rootStyles from 'app/pages/root/root.scss';
@@ -18,8 +18,10 @@ export const DevLayout: ComponentType = ({ children }) => (
export const sampleApp: OauthAppResponse = {
clientId: 'my-application',
clientSecret: 'cL1eNtS3cRE7xNJqfWQdqrMRKURfW1ssP4kiX6JDW0_szM-n-q',
type: TYPE_APPLICATION,
type: TYPE_WEB_APPLICATION,
name: 'My Application',
description: '',
redirectUri: '',
websiteUrl: '',
createdAt: 0,
countUsers: 0,

View File

@@ -1,7 +1,8 @@
import React from 'react';
import clsx from 'clsx';
import React, { FC, ReactElement } from 'react';
import { defineMessages, FormattedMessage as Message } from 'react-intl';
import { Helmet } from 'react-helmet-async';
import clsx from 'clsx';
import { LinkButton } from 'app/components/ui/form';
import { COLOR_GREEN, COLOR_BLUE } from 'app/components/ui';
import { ContactLink } from 'app/components/contact';
@@ -27,77 +28,83 @@ type Props = {
resetApp: (clientId: string, resetClientSecret: boolean) => Promise<any>;
};
export default class ApplicationsIndex extends React.Component<Props> {
render() {
return (
<div className={styles.container}>
<div className={styles.welcomeContainer}>
<Message key="accountsForDevelopers" defaultMessage="Ely.by Accounts for developers">
{(pageTitle: string) => (
<h2 className={styles.welcomeTitle}>
<Helmet title={pageTitle} />
{pageTitle}
</h2>
)}
</Message>
<div className={styles.welcomeTitleDelimiter} />
<div className={styles.welcomeParagraph}>
<Message
key="accountsAllowsYouYoUseOauth2"
defaultMessage="Ely.by Accounts service provides users with a quick and easy-to-use way to login to your site, launcher or Minecraft server via OAuth2 authorization protocol. You can find more information about integration with Ely.by Accounts in {ourDocumentation}."
values={{
ourDocumentation: (
<a href="https://docs.ely.by/en/oauth.html" target="_blank">
<Message key="ourDocumentation" defaultMessage="our documentation" />
</a>
),
}}
/>
</div>
<div className={styles.welcomeParagraph}>
<Message
key="ifYouHaveAnyTroubles"
defaultMessage="If you are experiencing difficulties, you can always use {feedback}. We'll surely help you."
values={{
feedback: (
<ContactLink>
<Message key="feedback" defaultMessage="feedback" />
</ContactLink>
),
}}
/>
</div>
</div>
const ApplicationsIndex: FC<Props> = ({
displayForGuest,
applications,
isLoading,
resetApp,
deleteApp,
clientId,
resetClientId,
}) => {
let content: ReactElement;
{this.getContent()}
</div>
if (applications.length > 0) {
content = (
<ApplicationsList
applications={applications}
resetApp={resetApp}
deleteApp={deleteApp}
clientId={clientId}
resetClientId={resetClientId}
/>
);
} else if (displayForGuest) {
content = <Guest />;
} else {
content = <Loader noApps={!isLoading} />;
}
getContent() {
const { displayForGuest, applications, isLoading, resetApp, deleteApp, clientId, resetClientId } = this.props;
return (
<div className={styles.container}>
<div className={styles.welcomeContainer}>
<Message key="accountsForDevelopers" defaultMessage="Ely.by Accounts for developers">
{(pageTitle: string) => (
<h2 className={styles.welcomeTitle}>
<Helmet title={pageTitle} />
{pageTitle}
</h2>
)}
</Message>
<div className={styles.welcomeTitleDelimiter} />
<div className={styles.welcomeParagraph}>
<Message
key="accountsAllowsYouYoUseOauth2"
defaultMessage="Ely.by Accounts service provides users with a quick and easy-to-use way to login to your site, launcher or Minecraft server via OAuth2 authorization protocol. You can find more information about integration with Ely.by Accounts in {ourDocumentation}."
values={{
ourDocumentation: (
<a href="https://docs.ely.by/en/oauth.html" target="_blank">
<Message key="ourDocumentation" defaultMessage="our documentation" />
</a>
),
}}
/>
</div>
<div className={styles.welcomeParagraph}>
<Message
key="ifYouHaveAnyTroubles"
defaultMessage="If you are experiencing difficulties, you can always use {feedback}. We'll surely help you."
values={{
feedback: (
<ContactLink>
<Message key="feedback" defaultMessage="feedback" />
</ContactLink>
),
}}
/>
</div>
</div>
if (applications.length > 0) {
return (
<ApplicationsList
applications={applications}
resetApp={resetApp}
deleteApp={deleteApp}
clientId={clientId}
resetClientId={resetClientId}
/>
);
}
{content}
</div>
);
};
if (displayForGuest) {
return <Guest />;
}
return <Loader noApps={!isLoading} />;
}
interface LoaderProps {
noApps: boolean;
}
function Loader({ noApps }: { noApps: boolean }) {
const Loader: FC<LoaderProps> = ({ noApps }) => {
return (
<div className={styles.emptyState} data-e2e={noApps ? 'noApps' : 'loading'}>
<img src={noApps ? cubeIcon : loadingCubeIcon} className={styles.emptyStateIcon} />
@@ -130,9 +137,9 @@ function Loader({ noApps }: { noApps: boolean }) {
</div>
</div>
);
}
};
function Guest() {
const Guest: FC = () => {
return (
<div className={styles.emptyState}>
<img src={toolsIcon} className={styles.emptyStateIcon} />
@@ -150,4 +157,6 @@ function Guest() {
</LinkButton>
</div>
);
}
};
export default ApplicationsIndex;

View File

@@ -4,14 +4,19 @@ import { action } from '@storybook/addon-actions';
import { OauthAppResponse } from 'app/services/api/oauth';
import { FormModel } from 'app/components/ui/form';
import { ApplicationType, TYPE_APPLICATION, TYPE_MINECRAFT_SERVER } from 'app/components/dev/apps';
import {
ApplicationType,
TYPE_WEB_APPLICATION,
TYPE_DESKTOP_APPLICATION,
TYPE_MINECRAFT_SERVER,
} from 'app/components/dev/apps';
import ApplicationForm from './ApplicationForm';
import { DevLayout } from '../ApplicationsIndex.story';
const blankApp: OauthAppResponse = {
clientId: '',
clientSecret: '',
type: TYPE_APPLICATION,
type: TYPE_WEB_APPLICATION,
name: '',
websiteUrl: '',
createdAt: 0,
@@ -47,7 +52,17 @@ storiesOf('Components/Dev/Apps/ApplicationForm', module)
form={new FormModel()}
onSubmit={onSubmit}
displayTypeSwitcher
type={TYPE_APPLICATION}
type={TYPE_WEB_APPLICATION}
setType={action('setType')}
app={blankApp}
/>
))
.add('Create desktop application', () => (
<ApplicationForm
form={new FormModel()}
onSubmit={onSubmit}
displayTypeSwitcher
type={TYPE_DESKTOP_APPLICATION}
setType={action('setType')}
app={blankApp}
/>
@@ -69,7 +84,7 @@ storiesOf('Components/Dev/Apps/ApplicationForm', module)
<ApplicationForm
form={new FormModel()}
onSubmit={onSubmit}
type={TYPE_APPLICATION}
type={TYPE_WEB_APPLICATION}
app={{
...blankApp,
clientId: 'already-registered',

View File

@@ -1,22 +1,28 @@
import React from 'react';
import { FormattedMessage as Message, defineMessages } from 'react-intl';
import React, { FC, useCallback } from 'react';
import { MessageDescriptor, FormattedMessage as Message, defineMessages } from 'react-intl';
import { Helmet } from 'react-helmet-async';
import { MessageDescriptor } from 'react-intl';
import { OauthAppResponse } from 'app/services/api/oauth';
import { ApplicationType } from 'app/components/dev/apps';
import {
ApplicationType,
TYPE_WEB_APPLICATION,
TYPE_DESKTOP_APPLICATION,
TYPE_MINECRAFT_SERVER,
} from 'app/components/dev/apps';
import { Form, FormModel, Button } from 'app/components/ui/form';
import { BackButton } from 'app/components/profile/ProfileForm';
import { COLOR_GREEN } from 'app/components/ui';
import { TYPE_APPLICATION, TYPE_MINECRAFT_SERVER } from 'app/components/dev/apps';
import styles from 'app/components/profile/profileForm.scss';
import logger from 'app/services/logger';
import ApplicationTypeSwitcher from './ApplicationTypeSwitcher';
import WebsiteType from './WebsiteType';
import DesktopApplicationType from './DesktopApplicationType';
import MinecraftServerType from './MinecraftServerType';
const messages = defineMessages({
website: 'Web site',
desktopApplication: 'Desktop application',
minecraftServer: 'Minecraft server',
creatingApplication: 'Creating an application',
@@ -32,10 +38,14 @@ type TypeToForm = Record<
>;
const typeToForm: TypeToForm = {
[TYPE_APPLICATION]: {
[TYPE_WEB_APPLICATION]: {
label: messages.website,
component: WebsiteType,
},
[TYPE_DESKTOP_APPLICATION]: {
label: messages.desktopApplication,
component: DesktopApplicationType,
},
[TYPE_MINECRAFT_SERVER]: {
label: messages.minecraftServer,
component: MinecraftServerType,
@@ -53,83 +63,22 @@ const typeToLabel: TypeToLabel = (Object.keys(typeToForm) as unknown as Array<Ap
{} as TypeToLabel,
);
export default class ApplicationForm extends React.Component<{
interface Props {
app: OauthAppResponse;
form: FormModel;
displayTypeSwitcher?: boolean;
type: ApplicationType | null;
setType: (type: ApplicationType) => void;
onSubmit: (form: FormModel) => Promise<void>;
}> {
static defaultProps = {
setType: () => {},
};
setType?: (type: ApplicationType) => void;
onSubmit?: (form: FormModel) => Promise<void>;
}
render() {
const { type, setType, form, displayTypeSwitcher, app } = this.props;
const { component: FormComponent } = (type && typeToForm[type]) || {};
const isUpdate = app.clientId !== '';
return (
<Form form={form} onSubmit={this.onFormSubmit}>
<div className={styles.contentWithBackButton}>
<BackButton to="/dev/applications" />
<div className={styles.form}>
<div className={styles.formBody}>
<Message {...(isUpdate ? messages.updatingApplication : messages.creatingApplication)}>
{(pageTitle: string) => (
<h3 className={styles.title}>
<Helmet title={pageTitle} />
{pageTitle}
</h3>
)}
</Message>
{displayTypeSwitcher && (
<div className={styles.formRow}>
<ApplicationTypeSwitcher
selectedType={type}
setType={setType}
appTypes={typeToLabel}
/>
</div>
)}
{FormComponent ? (
<FormComponent form={form} app={app} />
) : (
<div className={styles.formRow}>
<p className={styles.description}>
<Message
key="toDisplayRegistrationFormChooseType"
defaultMessage="To display registration form for a new application choose necessary type."
/>
</p>
</div>
)}
</div>
</div>
{!!FormComponent && (
<Button color={COLOR_GREEN} block type="submit">
{isUpdate ? (
<Message key="updateApplication" defaultMessage="Update application" />
) : (
<Message key="createApplication" defaultMessage="Create application" />
)}
</Button>
)}
</div>
</Form>
);
}
onFormSubmit = async () => {
const { form } = this.props;
const ApplicationForm: FC<Props> = ({ app, form, displayTypeSwitcher, type, setType, onSubmit }) => {
const isUpdate = app.clientId !== '';
const { component: FormComponent } = (type && typeToForm[type]) || {};
const onFormSubmit = useCallback(async () => {
try {
await this.props.onSubmit(form);
await onSubmit?.(form);
} catch (resp) {
if (resp.errors) {
form.setErrors(resp.errors);
@@ -139,5 +88,57 @@ export default class ApplicationForm extends React.Component<{
logger.unexpected(new Error('Error submitting application form'), resp);
}
};
}
}, [form, onSubmit]);
return (
<Form form={form} onSubmit={onFormSubmit}>
<div className={styles.contentWithBackButton}>
<BackButton to="/dev/applications" />
<div className={styles.form}>
<div className={styles.formBody}>
<Message {...(isUpdate ? messages.updatingApplication : messages.creatingApplication)}>
{(pageTitle: string) => (
<h3 className={styles.title}>
<Helmet title={pageTitle} />
{pageTitle}
</h3>
)}
</Message>
{displayTypeSwitcher && (
<div className={styles.formRow}>
<ApplicationTypeSwitcher selectedType={type} setType={setType} appTypes={typeToLabel} />
</div>
)}
{FormComponent ? (
<FormComponent form={form} app={app} />
) : (
<div className={styles.formRow}>
<p className={styles.description}>
<Message
key="toDisplayRegistrationFormChooseType"
defaultMessage="To display registration form for a new application choose necessary type."
/>
</p>
</div>
)}
</div>
</div>
{!!FormComponent && (
<Button color={COLOR_GREEN} block type="submit">
{isUpdate ? (
<Message key="updateApplication" defaultMessage="Update application" />
) : (
<Message key="createApplication" defaultMessage="Create application" />
)}
</Button>
)}
</div>
</Form>
);
};
export default ApplicationForm;

View File

@@ -9,7 +9,7 @@ import styles from './applicationTypeSwitcher.scss';
interface Props {
appTypes: Record<ApplicationType, MessageDescriptor>;
selectedType: ApplicationType | null;
setType: (type: ApplicationType) => void;
setType?: (type: ApplicationType) => void;
}
const ApplicationTypeSwitcher: ComponentType<Props> = ({ appTypes, selectedType, setType }) => (
@@ -17,7 +17,7 @@ const ApplicationTypeSwitcher: ComponentType<Props> = ({ appTypes, selectedType,
{(Object.keys(appTypes) as unknown as Array<ApplicationType>).map((type) => (
<div className={styles.radioContainer} key={type}>
<Radio
onChange={() => setType(type)}
onChange={() => setType?.(type)}
skin={SKIN_LIGHT}
label={appTypes[type]}
value={type}

View File

@@ -0,0 +1,59 @@
import React, { FC } from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { Input, TextArea, FormModel } from 'app/components/ui/form';
import { OauthDesktopAppResponse } from 'app/services/api/oauth';
import { SKIN_LIGHT } from 'app/components/ui';
import styles from 'app/components/profile/profileForm.scss';
import commonMessages from './commonMessages';
interface Props {
form: FormModel;
app: OauthDesktopAppResponse;
}
const DesktopApplicationType: FC<Props> = ({ form, app }) => (
<div>
<div className={styles.formRow}>
<Input
{...form.bindField('name')}
label={commonMessages.applicationName}
defaultValue={app.name}
required
skin={SKIN_LIGHT}
/>
</div>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...commonMessages.appDescriptionWillBeAlsoVisibleOnOauthPage} />
</p>
</div>
<div className={styles.formRow}>
<TextArea
{...form.bindField('description')}
label={commonMessages.description}
defaultValue={app.description}
skin={SKIN_LIGHT}
minRows={3}
/>
</div>
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...commonMessages.websiteLinkWillBeUsedAsAdditionalId} />
</p>
</div>
<div className={styles.formRow}>
<Input
{...form.bindField('websiteUrl')}
label={commonMessages.websiteLink}
defaultValue={app.websiteUrl}
skin={SKIN_LIGHT}
/>
</div>
</div>
);
export default DesktopApplicationType;

View File

@@ -1,22 +1,24 @@
import React, { ComponentType } from 'react';
import { FormattedMessage as Message, defineMessages } from 'react-intl';
import { OauthAppResponse } from 'app/services/api/oauth';
import { OauthMinecraftServerResponse } from 'app/services/api/oauth';
import { Input, FormModel } from 'app/components/ui/form';
import { SKIN_LIGHT } from 'app/components/ui';
import styles from 'app/components/profile/profileForm.scss';
import commonMessages from './commonMessages';
const messages = defineMessages({
serverName: 'Server name:',
ipAddressIsOptionButPreferable:
'IP address is optional, but is very preferable. It might become handy in case of we suddenly decide to play on your server with the entire band (=',
serverIp: 'Server IP:',
youCanAlsoSpecifyServerSite: "You also can specify either server's site URL or its community in a social network.",
websiteLink: 'Website link:',
});
interface Props {
form: FormModel;
app: OauthAppResponse;
app: OauthMinecraftServerResponse;
}
const MinecraftServerType: ComponentType<Props> = ({ form, app }) => (
@@ -53,7 +55,7 @@ const MinecraftServerType: ComponentType<Props> = ({ form, app }) => (
<div className={styles.formRow}>
<Input
{...form.bindField('websiteUrl')}
label={messages.websiteLink}
label={commonMessages.websiteLink}
defaultValue={app.websiteUrl}
skin={SKIN_LIGHT}
/>

View File

@@ -1,18 +1,13 @@
import React, { ComponentType } from 'react';
import { defineMessages, FormattedMessage as Message } from 'react-intl';
import { Input, TextArea, FormModel } from 'app/components/ui/form';
import { OauthAppResponse } from 'app/services/api/oauth';
import { OauthWebAppResponse } from 'app/services/api/oauth';
import { SKIN_LIGHT } from 'app/components/ui';
import styles from 'app/components/profile/profileForm.scss';
import commonMessages from './commonMessages';
const messages = defineMessages({
applicationName: 'Application name:',
appDescriptionWillBeAlsoVisibleOnOauthPage:
"Application's description will be displayed at the authorization page too. It isn't a required field. In authorization process the value may be overridden.",
description: 'Description:',
websiteLinkWillBeUsedAsAdditionalId:
"Site's link is optional, but it can be used as an additional identifier of the application.",
websiteLink: 'Website link:',
redirectUriLimitsAllowableBaseAddress:
"Redirection URI (redirectUri) determines a base address, that user will be allowed to be redirected to after authorization. In order to improve security it's better to use the whole path instead of just a domain name. For example: https://example.com/oauth/ely.",
redirectUri: 'Redirect URI:',
@@ -20,7 +15,7 @@ const messages = defineMessages({
interface Props {
form: FormModel;
app: OauthAppResponse;
app: OauthWebAppResponse;
}
const WebsiteType: ComponentType<Props> = ({ form, app }) => (
@@ -28,7 +23,7 @@ const WebsiteType: ComponentType<Props> = ({ form, app }) => (
<div className={styles.formRow}>
<Input
{...form.bindField('name')}
label={messages.applicationName}
label={commonMessages.applicationName}
defaultValue={app.name}
required
skin={SKIN_LIGHT}
@@ -37,13 +32,13 @@ const WebsiteType: ComponentType<Props> = ({ form, app }) => (
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.appDescriptionWillBeAlsoVisibleOnOauthPage} />
<Message {...commonMessages.appDescriptionWillBeAlsoVisibleOnOauthPage} />
</p>
</div>
<div className={styles.formRow}>
<TextArea
{...form.bindField('description')}
label={messages.description}
label={commonMessages.description}
defaultValue={app.description}
skin={SKIN_LIGHT}
minRows={3}
@@ -52,13 +47,13 @@ const WebsiteType: ComponentType<Props> = ({ form, app }) => (
<div className={styles.formRow}>
<p className={styles.description}>
<Message {...messages.websiteLinkWillBeUsedAsAdditionalId} />
<Message {...commonMessages.websiteLinkWillBeUsedAsAdditionalId} />
</p>
</div>
<div className={styles.formRow}>
<Input
{...form.bindField('websiteUrl')}
label={messages.websiteLink}
label={commonMessages.websiteLink}
defaultValue={app.websiteUrl}
skin={SKIN_LIGHT}
/>

View File

@@ -0,0 +1,11 @@
import { defineMessages } from 'react-intl';
export default defineMessages({
applicationName: 'Application name:',
appDescriptionWillBeAlsoVisibleOnOauthPage:
"Application's description will be displayed at the authorization page too. It isn't a required field. In authorization process the value may be overridden.",
description: 'Description:',
websiteLinkWillBeUsedAsAdditionalId:
"Site's link is optional, but it can be used as an additional identifier of the application.",
websiteLink: 'Website link:',
});

View File

@@ -1,3 +1,8 @@
export type ApplicationType = 'application' | 'minecraft-server';
export const TYPE_APPLICATION = 'application' as const;
export const TYPE_WEB_APPLICATION = 'application' as const;
export const TYPE_DESKTOP_APPLICATION = 'desktop-application' as const;
export const TYPE_MINECRAFT_SERVER = 'minecraft-server' as const;
export type ApplicationType =
| typeof TYPE_WEB_APPLICATION
| typeof TYPE_DESKTOP_APPLICATION
| typeof TYPE_MINECRAFT_SERVER;

View File

@@ -2,6 +2,7 @@ import React from 'react';
import { FormattedMessage as Message } from 'react-intl';
import { Link } from 'react-router-dom';
import clsx from 'clsx';
import { SKIN_LIGHT, COLOR_BLACK, COLOR_RED } from 'app/components/ui';
import { Input, Button } from 'app/components/ui/form';
import { OauthAppResponse } from 'app/services/api/oauth';
@@ -13,20 +14,6 @@ import messages from '../list.intl';
const ACTION_REVOKE_TOKENS = 'revoke-tokens';
const ACTION_RESET_SECRET = 'reset-secret';
const ACTION_DELETE = 'delete';
const actionButtons = [
{
type: ACTION_REVOKE_TOKENS,
label: <Message {...messages.revokeAllTokens} />,
},
{
type: ACTION_RESET_SECRET,
label: <Message {...messages.resetClientSecret} />,
},
{
type: ACTION_DELETE,
label: <Message {...messages.delete} />,
},
];
interface State {
selectedAction: string | null;
@@ -70,8 +57,9 @@ export default class ApplicationItem extends React.Component<
<div className={styles.appTileTitle}>
<div className={styles.appName}>{app.name}</div>
<div className={styles.appStats}>
{/* TODO: it must change items order for RTL languages, try to use FormattedList */}
Client ID: {app.clientId}
{typeof app.countUsers !== 'undefined' && (
{'countUsers' in app && (
<span>
{' | '}
<Message
@@ -103,34 +91,72 @@ export default class ApplicationItem extends React.Component<
<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}
data-testid="client-secret"
copy
/>
</div>
{'clientSecret' in app ? (
<>
<div className={styles.appDetailsInfoField}>
<Input
label="Client Secret:"
skin={SKIN_LIGHT}
disabled
value={app.clientSecret}
data-testid="client-secret"
copy
/>
</div>
<div className={styles.appDetailsDescription}>
<Message {...messages.ifYouSuspectingThatSecretHasBeenCompromised} />
</div>
<div className={styles.appDetailsDescription}>
<Message {...messages.ifYouSuspectingThatSecretHasBeenCompromised} />
</div>
</>
) : (
<div className={styles.appDetailsDescription}>
<Message
key="publickOauthApplicationDescription"
defaultMessage="This is a public client and it can't have any secrets by design. To secure authorization flow, use the PKCE challenge code. <link>Read more</link>."
values={{
// @ts-expect-error those typings seems to be invalid in the current version of react-intl, but might be fixed later
link: (nodes) => (
<a href="https://www.oauth.com/oauth2-servers/pkce/" target="_blank">
{nodes}
</a>
),
}}
/>
</div>
)}
<div className={styles.appActionsButtons}>
{actionButtons.map(({ type, label }) => (
<Button
color={COLOR_BLACK}
className={styles.appActionButton}
disabled={!!selectedAction && selectedAction !== ACTION_REVOKE_TOKENS}
onClick={this.onActionButtonClick(ACTION_REVOKE_TOKENS)}
small
>
<Message {...messages.revokeAllTokens} />
</Button>
{'clientSecret' in app && (
<Button
key={type}
color={COLOR_BLACK}
className={styles.appActionButton}
disabled={!!selectedAction && selectedAction !== type}
onClick={this.onActionButtonClick(type)}
disabled={!!selectedAction && selectedAction !== ACTION_RESET_SECRET}
onClick={this.onActionButtonClick(ACTION_RESET_SECRET)}
small
>
{label}
<Message {...messages.resetClientSecret} />
</Button>
))}
)}
<Button
color={COLOR_BLACK}
className={styles.appActionButton}
disabled={!!selectedAction && selectedAction !== ACTION_DELETE}
onClick={this.onActionButtonClick(ACTION_DELETE)}
small
>
<Message {...messages.delete} />
</Button>
</div>
<div

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { restoreScroll } from 'app/components/ui/scroll/scroll';
import { FormattedMessage as Message } from 'react-intl';
import { restoreScroll } from 'app/components/ui/scroll/scroll';
import { LinkButton } from 'app/components/ui/form';
import { COLOR_GREEN } from 'app/components/ui';
import { OauthAppResponse } from 'app/services/api/oauth';
@@ -9,24 +10,24 @@ import messages from '../list.intl';
import styles from '../applicationsIndex.scss';
import ApplicationItem from './ApplicationItem';
type Props = {
interface Props {
applications: OauthAppResponse[];
deleteApp: (clientId: string) => Promise<any>;
resetApp: (clientId: string, resetClientSecret: boolean) => Promise<any>;
resetClientId: () => void;
clientId: string | null;
};
}
type State = {
interface State {
expandedApp: string | null;
};
}
export default class ApplicationsList extends React.Component<Props, State> {
state = {
expandedApp: null,
};
appsRefs: { [key: string]: HTMLDivElement | null } = {};
appsRefs: Record<string, HTMLDivElement | null> = {};
componentDidMount() {
this.checkForActiveApp();

View File

@@ -9,21 +9,32 @@ export interface Client {
description: string;
}
export interface OauthAppResponse {
interface OauthCommonResponse {
clientId: string;
clientSecret: string;
type: ApplicationType;
name: string;
websiteUrl: string;
createdAt: number;
// fields for 'application' type
countUsers?: number;
description?: string;
redirectUri?: string;
// fields for 'minecraft-server' type
minecraftServerIp?: string;
}
export interface OauthWebAppResponse extends OauthCommonResponse {
clientSecret: string;
countUsers: number;
description: string;
redirectUri: string;
}
export interface OauthDesktopAppResponse extends OauthCommonResponse {
countUsers: number;
description: string;
}
export interface OauthMinecraftServerResponse extends OauthCommonResponse {
minecraftServerIp: string;
}
export type OauthAppResponse = OauthWebAppResponse | OauthDesktopAppResponse | OauthMinecraftServerResponse;
interface AuthCodeFlowRequestData {
client_id: string;
redirect_uri: string;