0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-06 20:40:08 -05:00

refactor(console): resources table (#2482)

This commit is contained in:
Xiao Yijun 2022-11-22 16:27:18 +08:00 committed by GitHub
parent 59c52feb2c
commit 6377c4837f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 327 additions and 319 deletions

View file

@ -86,58 +86,60 @@ const AuditLogTable = ({ userId }: Props) => {
/>
</div>
</div>
<div className={classNames(resourcesStyles.table, tableStyles.scrollable)}>
<table className={classNames(logs?.length === 0 && tableStyles.empty)}>
<colgroup>
<col className={styles.eventName} />
{showUserColumn && <col />}
<col />
<col />
</colgroup>
<thead>
<tr>
<th>{t('logs.event')}</th>
{showUserColumn && <th>{t('logs.user')}</th>}
<th>{t('logs.application')}</th>
<th>{t('logs.time')}</th>
</tr>
</thead>
<tbody>
{!data && error && (
<TableError
columns={4}
content={error.body?.message ?? error.message}
onRetry={async () => mutate(undefined, true)}
/>
)}
{isLoading && <TableLoading columns={4} />}
{logs?.length === 0 && <TableEmpty columns={4} />}
{logs?.map(({ type, payload, createdAt, id }) => (
<tr
key={id}
className={tableStyles.clickable}
onClick={() => {
navigate(`${pathname}/${id}`);
}}
>
<td>
<EventName type={type} isSuccess={payload.result === LogResult.Success} />
</td>
{showUserColumn && (
<td>{payload.userId ? <UserName userId={payload.userId} /> : '-'}</td>
)}
<td>
{payload.applicationId ? (
<ApplicationName applicationId={payload.applicationId} />
) : (
'-'
)}
</td>
<td>{new Date(createdAt).toLocaleString()}</td>
<div className={resourcesStyles.table}>
<div className={tableStyles.scrollable}>
<table className={classNames(logs?.length === 0 && tableStyles.empty)}>
<colgroup>
<col className={styles.eventName} />
{showUserColumn && <col />}
<col />
<col />
</colgroup>
<thead>
<tr>
<th>{t('logs.event')}</th>
{showUserColumn && <th>{t('logs.user')}</th>}
<th>{t('logs.application')}</th>
<th>{t('logs.time')}</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{!data && error && (
<TableError
columns={4}
content={error.body?.message ?? error.message}
onRetry={async () => mutate(undefined, true)}
/>
)}
{isLoading && <TableLoading columns={4} />}
{logs?.length === 0 && <TableEmpty columns={4} />}
{logs?.map(({ type, payload, createdAt, id }) => (
<tr
key={id}
className={tableStyles.clickable}
onClick={() => {
navigate(`${pathname}/${id}`);
}}
>
<td>
<EventName type={type} isSuccess={payload.result === LogResult.Success} />
</td>
{showUserColumn && (
<td>{payload.userId ? <UserName userId={payload.userId} /> : '-'}</td>
)}
<td>
{payload.applicationId ? (
<ApplicationName applicationId={payload.applicationId} />
) : (
'-'
)}
</td>
<td>{new Date(createdAt).toLocaleString()}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<Pagination
pageIndex={pageIndex}

View file

@ -77,65 +77,67 @@ const ApiResources = () => {
/>
</Modal>
</div>
<div className={classNames(resourcesStyles.table, tableStyles.scrollable)}>
<table className={classNames(!data && tableStyles.empty)}>
<colgroup>
<col className={styles.apiResourceName} />
<col />
</colgroup>
<thead>
<tr>
<th>{t('api_resources.api_name')}</th>
<th>{t('api_resources.api_identifier')}</th>
</tr>
</thead>
<tbody>
{!data && error && (
<TableError
columns={2}
content={error.body?.message ?? error.message}
onRetry={async () => mutate(undefined, true)}
/>
)}
{isLoading && <TableLoading columns={2} />}
{apiResources?.length === 0 && (
<TableEmpty columns={2}>
<Button
title="api_resources.create"
type="outline"
onClick={() => {
setIsCreateFormOpen(true);
}}
<div className={resourcesStyles.table}>
<div className={tableStyles.scrollable}>
<table className={classNames(!data && tableStyles.empty)}>
<colgroup>
<col className={styles.apiResourceName} />
<col />
</colgroup>
<thead>
<tr>
<th>{t('api_resources.api_name')}</th>
<th>{t('api_resources.api_identifier')}</th>
</tr>
</thead>
<tbody>
{!data && error && (
<TableError
columns={2}
content={error.body?.message ?? error.message}
onRetry={async () => mutate(undefined, true)}
/>
</TableEmpty>
)}
{apiResources?.map(({ id, name, indicator }) => {
const ResourceIcon =
theme === AppearanceMode.LightMode ? ApiResource : ApiResourceDark;
)}
{isLoading && <TableLoading columns={2} />}
{apiResources?.length === 0 && (
<TableEmpty columns={2}>
<Button
title="api_resources.create"
type="outline"
onClick={() => {
setIsCreateFormOpen(true);
}}
/>
</TableEmpty>
)}
{apiResources?.map(({ id, name, indicator }) => {
const ResourceIcon =
theme === AppearanceMode.LightMode ? ApiResource : ApiResourceDark;
return (
<tr
key={id}
className={tableStyles.clickable}
onClick={() => {
navigate(buildDetailsLink(id));
}}
>
<td>
<ItemPreview
title={name}
icon={<ResourceIcon className={styles.icon} />}
to={buildDetailsLink(id)}
/>
</td>
<td>
<CopyToClipboard value={indicator} variant="text" />
</td>
</tr>
);
})}
</tbody>
</table>
return (
<tr
key={id}
className={tableStyles.clickable}
onClick={() => {
navigate(buildDetailsLink(id));
}}
>
<td>
<ItemPreview
title={name}
icon={<ResourceIcon className={styles.icon} />}
to={buildDetailsLink(id)}
/>
</td>
<td>
<CopyToClipboard value={indicator} variant="text" />
</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
<Pagination
pageIndex={pageIndex}

View file

@ -70,63 +70,65 @@ const Applications = () => {
/>
</Modal>
</div>{' '}
<div className={classNames(resourcesStyles.table, tableStyles.scrollable)}>
<table className={classNames(!data && tableStyles.empty)}>
<colgroup>
<col className={styles.applicationName} />
<col />
</colgroup>
<div className={resourcesStyles.table}>
<div className={tableStyles.scrollable}>
<table className={classNames(!data && tableStyles.empty)}>
<colgroup>
<col className={styles.applicationName} />
<col />
</colgroup>
<thead>
<tr>
<th>{t('applications.application_name')}</th>
<th>{t('applications.app_id')}</th>
</tr>
</thead>
<tbody>
{!data && error && (
<TableError
columns={2}
content={error.body?.message ?? error.message}
onRetry={async () => mutate(undefined, true)}
/>
)}
{isLoading && <TableLoading columns={2} />}
{applications?.length === 0 && (
<TableEmpty columns={2}>
<Button
title="applications.create"
type="outline"
onClick={() => {
navigate('/applications/create');
}}
/>
</TableEmpty>
)}
{applications?.map(({ id, name, type }) => (
<tr
key={id}
className={tableStyles.clickable}
onClick={() => {
navigate(`/applications/${id}`);
}}
>
<td>
<ItemPreview
title={name}
subtitle={t(`${applicationTypeI18nKey[type]}.title`)}
icon={<ApplicationIcon className={styles.icon} type={type} />}
to={`/applications/${id}`}
/>
</td>
<td>
<CopyToClipboard value={id} variant="text" />
</td>
<thead>
<tr>
<th>{t('applications.application_name')}</th>
<th>{t('applications.app_id')}</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{!data && error && (
<TableError
columns={2}
content={error.body?.message ?? error.message}
onRetry={async () => mutate(undefined, true)}
/>
)}
{isLoading && <TableLoading columns={2} />}
{applications?.length === 0 && (
<TableEmpty columns={2}>
<Button
title="applications.create"
type="outline"
onClick={() => {
navigate('/applications/create');
}}
/>
</TableEmpty>
)}
{applications?.map(({ id, name, type }) => (
<tr
key={id}
className={tableStyles.clickable}
onClick={() => {
navigate(`/applications/${id}`);
}}
>
<td>
<ItemPreview
title={name}
subtitle={t(`${applicationTypeI18nKey[type]}.title`)}
icon={<ApplicationIcon className={styles.icon} type={type} />}
to={`/applications/${id}`}
/>
</td>
<td>
<CopyToClipboard value={id} variant="text" />
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<Pagination
pageIndex={pageIndex}

View file

@ -80,70 +80,72 @@ const Connectors = () => {
<TabNavItem href="/connectors">{t('connectors.tab_email_sms')}</TabNavItem>
<TabNavItem href="/connectors/social">{t('connectors.tab_social')}</TabNavItem>
</TabNav>
<div className={classNames(resourcesStyles.table, tableStyles.scrollable)}>
<table className={classNames(!data && tableStyles.empty)}>
<colgroup>
<col className={styles.connectorName} />
<col />
<col />
</colgroup>
<thead>
<tr>
<th>{t('connectors.connector_name')}</th>
<th>{t('connectors.connector_type')}</th>
<th>
<ConnectorStatusField />
</th>
</tr>
</thead>
<tbody>
{!data && error && (
<TableError
columns={3}
content={error.body?.message ?? error.message}
onRetry={async () => mutate(undefined, true)}
/>
)}
{isLoading && <TableLoading columns={3} />}
{socialConnectorGroups?.length === 0 && (
<TableEmpty
columns={3}
title={t('connectors.type.social')}
content={t('connectors.social_connector_eg')}
image={isLightMode ? <SocialConnectorEmpty /> : <SocialConnectorEmptyDark />}
>
<Button
title="connectors.create"
type="outline"
onClick={() => {
setCreateType(ConnectorType.Social);
<div className={resourcesStyles.table}>
<div className={tableStyles.scrollable}>
<table className={classNames(!data && tableStyles.empty)}>
<colgroup>
<col className={styles.connectorName} />
<col />
<col />
</colgroup>
<thead>
<tr>
<th>{t('connectors.connector_name')}</th>
<th>{t('connectors.connector_type')}</th>
<th>
<ConnectorStatusField />
</th>
</tr>
</thead>
<tbody>
{!data && error && (
<TableError
columns={3}
content={error.body?.message ?? error.message}
onRetry={async () => mutate(undefined, true)}
/>
)}
{isLoading && <TableLoading columns={3} />}
{socialConnectorGroups?.length === 0 && (
<TableEmpty
columns={3}
title={t('connectors.type.social')}
content={t('connectors.social_connector_eg')}
image={isLightMode ? <SocialConnectorEmpty /> : <SocialConnectorEmptyDark />}
>
<Button
title="connectors.create"
type="outline"
onClick={() => {
setCreateType(ConnectorType.Social);
}}
/>
</TableEmpty>
)}
{!isLoading && !isSocial && (
<ConnectorRow
connectors={smsConnector ? [smsConnector] : []}
type={ConnectorType.Sms}
onClickSetup={() => {
setCreateType(ConnectorType.Sms);
}}
/>
</TableEmpty>
)}
{!isLoading && !isSocial && (
<ConnectorRow
connectors={smsConnector ? [smsConnector] : []}
type={ConnectorType.Sms}
onClickSetup={() => {
setCreateType(ConnectorType.Sms);
}}
/>
)}
{!isLoading && !isSocial && (
<ConnectorRow
connectors={emailConnector ? [emailConnector] : []}
type={ConnectorType.Email}
onClickSetup={() => {
setCreateType(ConnectorType.Email);
}}
/>
)}
{socialConnectorGroups?.map(({ connectors, id }) => (
<ConnectorRow key={id} connectors={connectors} type={ConnectorType.Social} />
))}
</tbody>
</table>
)}
{!isLoading && !isSocial && (
<ConnectorRow
connectors={emailConnector ? [emailConnector] : []}
type={ConnectorType.Email}
onClickSetup={() => {
setCreateType(ConnectorType.Email);
}}
/>
)}
{socialConnectorGroups?.map(({ connectors, id }) => (
<ConnectorRow key={id} connectors={connectors} type={ConnectorType.Social} />
))}
</tbody>
</table>
</div>
</div>
</div>
<CreateForm

View file

@ -5,19 +5,7 @@
}
.tableContainer {
flex: 1;
background-color: var(--color-layer-1);
> table {
> tbody {
> tr {
> td {
padding-top: 10px;
padding-bottom: 10px;
}
}
}
}
margin-top: 0;
}
.pagination {

View file

@ -87,71 +87,73 @@ const Users = () => {
}}
/>
</div>
<div className={classNames(styles.tableContainer, tableStyles.scrollable)}>
<table className={classNames(!data && tableStyles.empty)}>
<colgroup>
<col className={styles.userName} />
<col />
<col />
</colgroup>
<thead>
<tr>
<th>{t('users.user_name')}</th>
<th>{t('users.application_name')}</th>
<th>{t('users.latest_sign_in')}</th>
</tr>
</thead>
<tbody>
{!data && error && (
<TableError
columns={3}
content={error.body?.message ?? error.message}
onRetry={async () => mutate(undefined, true)}
/>
)}
{isLoading && <TableLoading columns={3} />}
{users?.length === 0 && (
<TableEmpty columns={3}>
<Button
title="users.create"
type="outline"
onClick={() => {
setIsCreateFormOpen(true);
}}
/>
</TableEmpty>
)}
{users?.map(({ id, name, avatar, lastSignInAt, applicationId }) => (
<tr
key={id}
className={tableStyles.clickable}
onClick={() => {
navigate(`/users/${id}`);
}}
>
<td>
<ItemPreview
title={name ?? t('users.unnamed')}
subtitle={id}
icon={
<img
alt="avatar"
className={styles.avatar}
src={avatar ?? generateAvatarPlaceHolderById(id)}
/>
}
to={`/users/${id}`}
size="compact"
/>
</td>
<td>{applicationId ? <ApplicationName applicationId={applicationId} /> : '-'}</td>
<td>
<DateTime>{lastSignInAt}</DateTime>
</td>
<div className={classNames(resourcesStyles.table, styles.tableContainer)}>
<div className={tableStyles.scrollable}>
<table className={classNames(!data && tableStyles.empty)}>
<colgroup>
<col className={styles.userName} />
<col />
<col />
</colgroup>
<thead>
<tr>
<th>{t('users.user_name')}</th>
<th>{t('users.application_name')}</th>
<th>{t('users.latest_sign_in')}</th>
</tr>
))}
</tbody>
</table>
</thead>
<tbody>
{!data && error && (
<TableError
columns={3}
content={error.body?.message ?? error.message}
onRetry={async () => mutate(undefined, true)}
/>
)}
{isLoading && <TableLoading columns={3} />}
{users?.length === 0 && (
<TableEmpty columns={3}>
<Button
title="users.create"
type="outline"
onClick={() => {
setIsCreateFormOpen(true);
}}
/>
</TableEmpty>
)}
{users?.map(({ id, name, avatar, lastSignInAt, applicationId }) => (
<tr
key={id}
className={tableStyles.clickable}
onClick={() => {
navigate(`/users/${id}`);
}}
>
<td>
<ItemPreview
title={name ?? t('users.unnamed')}
subtitle={id}
icon={
<img
alt="avatar"
className={styles.avatar}
src={avatar ?? generateAvatarPlaceHolderById(id)}
/>
}
to={`/users/${id}`}
size="compact"
/>
</td>
<td>{applicationId ? <ApplicationName applicationId={applicationId} /> : '-'}</td>
<td>
<DateTime>{lastSignInAt}</DateTime>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
<Pagination
pageIndex={pageIndex}

View file

@ -2,17 +2,19 @@
.container {
width: 100%;
display: flex;
flex-direction: column;
padding-bottom: _.unit(6);
@include _.flex-column;
.headline {
display: flex;
justify-content: space-between;
}
.table {
flex: 1;
margin-top: _.unit(4);
background-color: var(--color-layer-1);
}
height: 100%;
}
.headline {
display: flex;
justify-content: space-between;
}
.table {
flex: 1;
margin-top: _.unit(4);
overflow: hidden;
}

View file

@ -13,20 +13,28 @@ tr.clickable {
}
.scrollable {
overflow-y: auto;
border: 1px solid var(--color-divider);
max-height: 100%;
background-color: var(--color-layer-1);
border-radius: 12px;
overflow-y: auto;
table {
border: none;
box-shadow: none;
thead tr {
thead {
background: var(--color-layer-1);
position: sticky;
top: 0;
}
th {
background: var(--color-layer-1);
tbody {
tr {
&:last-child {
td {
border-bottom: unset;
}
}
}
}
}