mirror of
https://github.com/logto-io/logto.git
synced 2025-03-10 22:22:45 -05:00
Merge pull request #1102 from logto-io/charles-ac-misc-improvements
fix(console): misc improvements and ui fixes
This commit is contained in:
commit
1d2272186e
21 changed files with 107 additions and 42 deletions
|
@ -82,7 +82,8 @@
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"extends": "@silverhand/react",
|
"extends": "@silverhand/react",
|
||||||
"rules": {
|
"rules": {
|
||||||
"complexity": "off"
|
"complexity": "off",
|
||||||
|
"@typescript-eslint/prefer-nullish-coalescing": "off"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"stylelint": {
|
"stylelint": {
|
||||||
|
|
|
@ -6,6 +6,7 @@
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 248px;
|
width: 248px;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
> div + div {
|
> div + div {
|
||||||
margin-top: _.unit(6);
|
margin-top: _.unit(6);
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { useLogto, UserInfoResponse } from '@logto/react';
|
import { useLogto, UserInfoResponse } from '@logto/react';
|
||||||
import { UserRole } from '@logto/schemas';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React, { useEffect, useRef, useState, MouseEvent } from 'react';
|
import React, { useEffect, useRef, useState, MouseEvent } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
@ -10,6 +9,7 @@ import { getAvatarById } from '@/consts/avatars';
|
||||||
import useApi from '@/hooks/use-api';
|
import useApi from '@/hooks/use-api';
|
||||||
import SignOut from '@/icons/SignOut';
|
import SignOut from '@/icons/SignOut';
|
||||||
|
|
||||||
|
import UserInfoSkeleton from '../UserInfoSkeleton';
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
const UserInfo = () => {
|
const UserInfo = () => {
|
||||||
|
@ -31,11 +31,10 @@ const UserInfo = () => {
|
||||||
}, [api, isAuthenticated, fetchUserInfo]);
|
}, [api, isAuthenticated, fetchUserInfo]);
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return null;
|
return <UserInfoSkeleton />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { sub: id, name, avatar, role_names: roleNames } = user;
|
const { sub: id, name, avatar } = user;
|
||||||
const isAdmin = roleNames?.includes(UserRole.Admin);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -48,8 +47,7 @@ const UserInfo = () => {
|
||||||
>
|
>
|
||||||
<img src={avatar ?? getAvatarById(id)} />
|
<img src={avatar ?? getAvatarById(id)} />
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.wrapper}>
|
||||||
<div className={styles.name}>{name}</div>
|
<div className={styles.name}>{name || t('users.unnamed')}</div>
|
||||||
{isAdmin && <div className={styles.role}>{t('user_details.roles.admin')}</div>}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
@use '@/scss/underscore' as _;
|
||||||
|
|
||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: _.unit(2);
|
||||||
|
margin-left: _.unit(4);
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
.image {
|
||||||
|
@include _.shimmering-animation;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
margin-right: _.unit(2);
|
||||||
|
border-radius: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
@include _.shimmering-animation;
|
||||||
|
width: 85px;
|
||||||
|
height: 20px;
|
||||||
|
color: var(--color-text);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
|
const UserInfoSkeleton = () => (
|
||||||
|
<div className={styles.container}>
|
||||||
|
<div className={styles.image} />
|
||||||
|
<div className={styles.name} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default UserInfoSkeleton;
|
|
@ -16,7 +16,6 @@ const ApplicationName = ({ applicationId, isLink = false }: Props) => {
|
||||||
|
|
||||||
const { data } = useSWR<Application>(!isAdminConsole && `/api/applications/${applicationId}`);
|
const { data } = useSWR<Application>(!isAdminConsole && `/api/applications/${applicationId}`);
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
|
|
||||||
const name = (isAdminConsole ? 'Admin Console' : data?.name) || '-';
|
const name = (isAdminConsole ? 'Admin Console' : data?.name) || '-';
|
||||||
|
|
||||||
if (isLink && !isAdminConsole) {
|
if (isLink && !isAdminConsole) {
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
padding: _.unit(6);
|
padding: _.unit(6);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
// Force dark theme on the code editor
|
// Force dark theme on the code editor
|
||||||
background: #2d3132;
|
background: #34353f;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.copy {
|
.copy {
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
import React, { ChangeEvent } from 'react';
|
import React, { ChangeEvent } from 'react';
|
||||||
import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter';
|
import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter';
|
||||||
import { a11yDark as theme } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
import { a11yDark as theme } from 'react-syntax-highlighter/dist/esm/styles/prism';
|
||||||
|
@ -6,19 +7,20 @@ import CopyToClipboard from '../CopyToClipboard';
|
||||||
import * as styles from './index.module.scss';
|
import * as styles from './index.module.scss';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
className?: string;
|
||||||
language?: string;
|
language?: string;
|
||||||
isReadonly?: boolean;
|
isReadonly?: boolean;
|
||||||
value?: string;
|
value?: string;
|
||||||
onChange?: (value: string) => void;
|
onChange?: (value: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
const CodeEditor = ({ language, isReadonly = false, value = '', onChange }: Props) => {
|
const CodeEditor = ({ className, language, isReadonly = false, value = '', onChange }: Props) => {
|
||||||
const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
const handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
onChange?.(event.currentTarget.value);
|
onChange?.(event.currentTarget.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={classNames(styles.container, className)}>
|
||||||
<CopyToClipboard value={value} variant="icon" className={styles.copy} />
|
<CopyToClipboard value={value} variant="icon" className={styles.copy} />
|
||||||
<div className={styles.editor}>
|
<div className={styles.editor}>
|
||||||
{/* SyntaxHighlighter is a readonly component, so a transparent <textarea> layer is needed
|
{/* SyntaxHighlighter is a readonly component, so a transparent <textarea> layer is needed
|
||||||
|
|
|
@ -26,25 +26,14 @@
|
||||||
cursor: text;
|
cursor: text;
|
||||||
|
|
||||||
.copyIcon {
|
.copyIcon {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
margin-left: _.unit(3);
|
margin-left: _.unit(3);
|
||||||
|
color: var(--color-icon);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
> svg {
|
&:hover {
|
||||||
color: var(--color-icon);
|
color: var(--color-text);
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.icon {
|
|
||||||
.row {
|
|
||||||
.copyIcon {
|
|
||||||
> svg {
|
|
||||||
color: rgba(#fff, 0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
:hover {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,7 +18,7 @@ const UserName = ({ userId, isLink = false }: Props) => {
|
||||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||||
|
|
||||||
const isLoading = !data && !error;
|
const isLoading = !data && !error;
|
||||||
const name = data?.name ?? t('users.unnamed');
|
const name = data?.name || t('users.unnamed');
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return null;
|
return null;
|
||||||
|
|
|
@ -48,6 +48,7 @@ const ApiResources = () => {
|
||||||
<Button
|
<Button
|
||||||
title="admin_console.api_resources.create"
|
title="admin_console.api_resources.create"
|
||||||
type="primary"
|
type="primary"
|
||||||
|
size="large"
|
||||||
icon={<Plus />}
|
icon={<Plus />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setIsCreateFormOpen(true);
|
setIsCreateFormOpen(true);
|
||||||
|
|
|
@ -49,6 +49,7 @@ const Applications = () => {
|
||||||
icon={<Plus />}
|
icon={<Plus />}
|
||||||
title="admin_console.applications.create"
|
title="admin_console.applications.create"
|
||||||
type="primary"
|
type="primary"
|
||||||
|
size="large"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate('/applications/create');
|
navigate('/applications/create');
|
||||||
}}
|
}}
|
||||||
|
|
|
@ -10,7 +10,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.send {
|
.send {
|
||||||
margin-left: _.unit(1);
|
margin-left: _.unit(1.5);
|
||||||
margin-bottom: 1px;
|
margin-bottom: 1px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -78,5 +78,9 @@
|
||||||
|
|
||||||
.main {
|
.main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
|
.codeEditor {
|
||||||
|
margin-bottom: _.unit(6);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -206,6 +206,7 @@ const ConnectorDetails = () => {
|
||||||
</TabNav>
|
</TabNav>
|
||||||
<div className={styles.main}>
|
<div className={styles.main}>
|
||||||
<CodeEditor
|
<CodeEditor
|
||||||
|
className={styles.codeEditor}
|
||||||
language="json"
|
language="json"
|
||||||
value={config}
|
value={config}
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
|
@ -219,6 +220,7 @@ const ConnectorDetails = () => {
|
||||||
<div className={detailsStyles.footerMain}>
|
<div className={detailsStyles.footerMain}>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
|
size="large"
|
||||||
title="admin_console.connector_details.save_changes"
|
title="admin_console.connector_details.save_changes"
|
||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
onClick={handleSave}
|
onClick={handleSave}
|
||||||
|
|
|
@ -28,10 +28,16 @@ a.link {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
&:not(:last-child) {
|
&:not(:last-child)::after {
|
||||||
padding-right: _.unit(1);
|
content: '';
|
||||||
margin-right: _.unit(1);
|
width: 0;
|
||||||
|
height: 12px;
|
||||||
border-right: 1px solid var(--color-border);
|
border-right: 1px solid var(--color-border);
|
||||||
|
margin: 0 _.unit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
svg {
|
||||||
|
margin-right: _.unit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -60,6 +60,7 @@ const Connectors = () => {
|
||||||
<Button
|
<Button
|
||||||
title="admin_console.connectors.create"
|
title="admin_console.connectors.create"
|
||||||
type="primary"
|
type="primary"
|
||||||
|
size="large"
|
||||||
icon={<Plus />}
|
icon={<Plus />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setCreateType(ConnectorType.Social);
|
setCreateType(ConnectorType.Social);
|
||||||
|
|
|
@ -135,6 +135,7 @@ const SignInExperience = () => {
|
||||||
<Button
|
<Button
|
||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
type="primary"
|
type="primary"
|
||||||
|
size="large"
|
||||||
htmlType="submit"
|
htmlType="submit"
|
||||||
title="general.save_changes"
|
title="general.save_changes"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
tbody {
|
tbody {
|
||||||
tr {
|
tr {
|
||||||
td {
|
td {
|
||||||
padding: _.unit(2);
|
padding: _.unit(3) _.unit(2);
|
||||||
font: var(--font-body-medium);
|
font: var(--font-body-medium);
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
|
@ -53,4 +53,19 @@
|
||||||
margin-left: _.unit(3);
|
margin-left: _.unit(3);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.connectorId {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font: var(--font-body-medium);
|
||||||
|
font-family: 'Roboto Mono', monospace;
|
||||||
|
line-height: 32px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
max-width: 220px;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr';
|
||||||
|
|
||||||
import Button from '@/components/Button';
|
import Button from '@/components/Button';
|
||||||
|
import CopyToClipboard from '@/components/CopyToClipboard';
|
||||||
import TableError from '@/components/Table/TableError';
|
import TableError from '@/components/Table/TableError';
|
||||||
import UnnamedTrans from '@/components/UnnamedTrans';
|
import UnnamedTrans from '@/components/UnnamedTrans';
|
||||||
import useApi, { RequestError } from '@/hooks/use-api';
|
import useApi, { RequestError } from '@/hooks/use-api';
|
||||||
|
@ -87,6 +88,11 @@ const UserConnectors = ({ userId, connectors, onDelete }: Props) => {
|
||||||
{isLoading && <div>Loading</div>}
|
{isLoading && <div>Loading</div>}
|
||||||
{displayConnectors && (
|
{displayConnectors && (
|
||||||
<table className={styles.table}>
|
<table className={styles.table}>
|
||||||
|
<colgroup>
|
||||||
|
<col width="156px" />
|
||||||
|
<col />
|
||||||
|
<col width="110px" />
|
||||||
|
</colgroup>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{t('user_details.connectors.connectors')}</th>
|
<th>{t('user_details.connectors.connectors')}</th>
|
||||||
|
@ -102,25 +108,26 @@ const UserConnectors = ({ userId, connectors, onDelete }: Props) => {
|
||||||
onRetry={async () => mutate(undefined, true)}
|
onRetry={async () => mutate(undefined, true)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{displayConnectors.map((connector) => (
|
{displayConnectors.map(({ id, userId = '', name, logo }) => (
|
||||||
<tr key={connector.id}>
|
<tr key={id}>
|
||||||
<td>
|
<td>
|
||||||
<div className={styles.connectorName}>
|
<div className={styles.connectorName}>
|
||||||
<div>
|
<img src={logo} />
|
||||||
<img src={connector.logo} />
|
|
||||||
</div>
|
|
||||||
<div className={styles.name}>
|
<div className={styles.name}>
|
||||||
<UnnamedTrans resource={connector.name} />
|
<UnnamedTrans resource={name} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td>{connector.userId}</td>
|
<td className={styles.connectorId}>
|
||||||
|
<span>{userId || '-'}</span>
|
||||||
|
<CopyToClipboard variant="icon" value={userId} />
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<Button
|
<Button
|
||||||
title="admin_console.user_details.connectors.remove"
|
title="admin_console.user_details.connectors.remove"
|
||||||
type="plain"
|
type="plain"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
void handleDelete(connector.id);
|
void handleDelete(id);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -50,6 +50,7 @@ const Users = () => {
|
||||||
<CardTitle title="users.title" subtitle="users.subtitle" />
|
<CardTitle title="users.title" subtitle="users.subtitle" />
|
||||||
<Button
|
<Button
|
||||||
title="admin_console.users.create"
|
title="admin_console.users.create"
|
||||||
|
size="large"
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<Plus />}
|
icon={<Plus />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
@ -124,7 +125,7 @@ const Users = () => {
|
||||||
>
|
>
|
||||||
<td>
|
<td>
|
||||||
<ItemPreview
|
<ItemPreview
|
||||||
title={name ?? t('users.unnamed')}
|
title={name || t('users.unnamed')}
|
||||||
subtitle={conditionalString(username)}
|
subtitle={conditionalString(username)}
|
||||||
icon={<img className={styles.avatar} src={avatar ?? getAvatarById(id)} />}
|
icon={<img className={styles.avatar} src={avatar ?? getAvatarById(id)} />}
|
||||||
to={`/users/${id}`}
|
to={`/users/${id}`}
|
||||||
|
|
Loading…
Add table
Reference in a new issue