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