mirror of
https://github.com/logto-io/logto.git
synced 2025-03-31 22:51:25 -05:00
refactor(console): improve app creation page empty state layout (#5310)
This commit is contained in:
parent
47d1515d8d
commit
aee90d0a6f
11 changed files with 66 additions and 29 deletions
|
@ -39,4 +39,12 @@
|
|||
height: 128px;
|
||||
}
|
||||
}
|
||||
|
||||
.topSpace {
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.bottomSpace {
|
||||
flex: 3;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,8 +22,10 @@ function EmptyDataPlaceholder({ title, size = 'medium', className }: Props) {
|
|||
|
||||
return (
|
||||
<div className={classNames(styles.empty, styles[size], className)}>
|
||||
<div className={styles.topSpace} />
|
||||
<EmptyImage className={styles.image} />
|
||||
<div className={styles.title}>{title ?? t('errors.empty')}</div>
|
||||
<div className={styles.bottomSpace} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
.wrapper {
|
||||
width: 100%;
|
||||
min-height: 100%;
|
||||
min-width: dim.$guide-content-min-width;
|
||||
max-width: dim.$guide-content-max-width;
|
||||
margin: 0 auto;
|
||||
|
@ -87,10 +88,13 @@
|
|||
}
|
||||
|
||||
.emptyPlaceholder {
|
||||
justify-content: center;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 70%;
|
||||
height: calc(100vh - 188px);
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.viewAll {
|
||||
margin-top: _.unit(8);
|
||||
}
|
||||
|
||||
@media screen and (max-width: dim.$guide-content-max-width) {
|
||||
|
|
|
@ -1,17 +1,19 @@
|
|||
import { type Application } from '@logto/schemas';
|
||||
import { ApplicationType, type Application } from '@logto/schemas';
|
||||
import classNames from 'classnames';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import SearchIcon from '@/assets/icons/search.svg';
|
||||
import EmptyDataPlaceholder from '@/components/EmptyDataPlaceholder';
|
||||
import { type SelectedGuide } from '@/components/Guide/GuideCard';
|
||||
import GuideCardGroup from '@/components/Guide/GuideCardGroup';
|
||||
import { useAppGuideMetadata } from '@/components/Guide/hooks';
|
||||
import { isDevFeaturesEnabled } from '@/consts/env';
|
||||
import { isCloud, isDevFeaturesEnabled } from '@/consts/env';
|
||||
import { CheckboxGroup } from '@/ds-components/Checkbox';
|
||||
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
|
||||
import TextInput from '@/ds-components/TextInput';
|
||||
import TextLink from '@/ds-components/TextLink';
|
||||
import useTenantPathname from '@/hooks/use-tenant-pathname';
|
||||
import { allAppGuideCategories, type AppGuideCategory } from '@/types/applications';
|
||||
import { thirdPartyAppCategory } from '@/types/applications';
|
||||
|
@ -25,17 +27,18 @@ type Props = {
|
|||
className?: string;
|
||||
hasCardBorder?: boolean;
|
||||
hasCardButton?: boolean;
|
||||
hasFilters?: boolean;
|
||||
};
|
||||
|
||||
function GuideLibrary({ className, hasCardBorder, hasCardButton, hasFilters }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.guide' });
|
||||
function GuideLibrary({ className, hasCardBorder, hasCardButton }: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
|
||||
const { navigate } = useTenantPathname();
|
||||
const { pathname } = useLocation();
|
||||
const [keyword, setKeyword] = useState<string>('');
|
||||
const [filterCategories, setFilterCategories] = useState<AppGuideCategory[]>([]);
|
||||
const [selectedGuide, setSelectedGuide] = useState<SelectedGuide>();
|
||||
const { getFilteredAppGuideMetadata, getStructuredAppGuideMetadata } = useAppGuideMetadata();
|
||||
const [showCreateForm, setShowCreateForm] = useState<boolean>(false);
|
||||
const isApplicationCreateModal = pathname.includes('/applications/create');
|
||||
|
||||
const structuredMetadata = useMemo(
|
||||
() => getStructuredAppGuideMetadata({ categories: filterCategories }),
|
||||
|
@ -72,16 +75,16 @@ function GuideLibrary({ className, hasCardBorder, hasCardButton, hasFilters }: P
|
|||
|
||||
return (
|
||||
<OverlayScrollbar className={classNames(styles.container, className)}>
|
||||
<div className={classNames(styles.wrapper, hasFilters && styles.hasFilters)}>
|
||||
<div className={classNames(styles.wrapper, isApplicationCreateModal && styles.hasFilters)}>
|
||||
<div className={styles.groups}>
|
||||
{hasFilters && (
|
||||
{isApplicationCreateModal && (
|
||||
<div className={styles.filterAnchor}>
|
||||
<div className={styles.filters}>
|
||||
<label>{t('filter.title')}</label>
|
||||
<label>{t('guide.filter.title')}</label>
|
||||
<TextInput
|
||||
className={styles.searchInput}
|
||||
icon={<SearchIcon />}
|
||||
placeholder={t('filter.placeholder')}
|
||||
placeholder={t('guide.filter.placeholder')}
|
||||
value={keyword}
|
||||
onChange={(event) => {
|
||||
setKeyword(event.currentTarget.value);
|
||||
|
@ -92,9 +95,7 @@ function GuideLibrary({ className, hasCardBorder, hasCardButton, hasFilters }: P
|
|||
className={styles.checkboxGroup}
|
||||
options={allAppGuideCategories
|
||||
.filter(
|
||||
(category) =>
|
||||
category !== 'Protected' &&
|
||||
(isDevFeaturesEnabled || category !== thirdPartyAppCategory)
|
||||
(category) => isDevFeaturesEnabled || category !== thirdPartyAppCategory
|
||||
)
|
||||
.map((category) => ({
|
||||
title: `guide.categories.${category}`,
|
||||
|
@ -126,9 +127,17 @@ function GuideLibrary({ className, hasCardBorder, hasCardButton, hasFilters }: P
|
|||
))}
|
||||
{!keyword && (
|
||||
<>
|
||||
{isDevFeaturesEnabled && (
|
||||
<ProtectedAppCard hasLabel hasCreateButton className={styles.protectedAppCard} />
|
||||
)}
|
||||
{isDevFeaturesEnabled &&
|
||||
isCloud &&
|
||||
(filterCategories.length === 0 ||
|
||||
filterCategories.includes(ApplicationType.Protected)) && (
|
||||
<ProtectedAppCard
|
||||
hasLabel
|
||||
hasCreateButton
|
||||
hasBorder={hasCardBorder}
|
||||
className={styles.protectedAppCard}
|
||||
/>
|
||||
)}
|
||||
{(filterCategories.length > 0 ? filterCategories : allAppGuideCategories).map(
|
||||
(category) =>
|
||||
structuredMetadata[category].length > 0 && (
|
||||
|
@ -137,7 +146,7 @@ function GuideLibrary({ className, hasCardBorder, hasCardButton, hasFilters }: P
|
|||
className={styles.guideGroup}
|
||||
hasCardBorder={hasCardBorder}
|
||||
hasCardButton={hasCardButton}
|
||||
categoryName={t(`categories.${category}`)}
|
||||
categoryName={t(`guide.categories.${category}`)}
|
||||
guides={structuredMetadata[category]}
|
||||
onClickGuide={onClickGuide}
|
||||
/>
|
||||
|
@ -145,6 +154,11 @@ function GuideLibrary({ className, hasCardBorder, hasCardButton, hasFilters }: P
|
|||
)}
|
||||
</>
|
||||
)}
|
||||
{!isApplicationCreateModal && (
|
||||
<TextLink className={styles.viewAll} to="/applications/create">
|
||||
{t('get_started.view_all')}
|
||||
</TextLink>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{selectedGuide?.target !== 'API' && showCreateForm && (
|
||||
|
|
|
@ -36,7 +36,7 @@ function GuideLibraryModal({ isOpen, onClose }: Props) {
|
|||
requestSuccessMessage="guide.request_guide_successfully"
|
||||
onClose={onClose}
|
||||
/>
|
||||
<GuideLibrary hasFilters hasCardButton className={styles.content} />
|
||||
<GuideLibrary hasCardButton className={styles.content} />
|
||||
<ModalFooter
|
||||
wrapperClassName={styles.footerInnerWrapper}
|
||||
content="guide.do_not_need_tutorial"
|
||||
|
|
|
@ -21,6 +21,10 @@
|
|||
background-color: var(--color-layer-1);
|
||||
border-radius: 12px;
|
||||
|
||||
&.hasBorder {
|
||||
border: 1px solid var(--color-divider);
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
|
|
@ -16,12 +16,19 @@ import * as styles from './index.module.scss';
|
|||
|
||||
type Props = {
|
||||
className?: string;
|
||||
hasBorder?: boolean;
|
||||
hasLabel?: boolean;
|
||||
hasCreateButton?: boolean;
|
||||
onCreateSuccess?: (app: Application) => void;
|
||||
};
|
||||
|
||||
function ProtectedAppCard({ className, hasLabel, hasCreateButton, onCreateSuccess }: Props) {
|
||||
function ProtectedAppCard({
|
||||
className,
|
||||
hasBorder,
|
||||
hasLabel,
|
||||
hasCreateButton,
|
||||
onCreateSuccess,
|
||||
}: Props) {
|
||||
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console.protected_app' });
|
||||
const { documentationSiteUrl } = useDocumentationUrl();
|
||||
const [showCreateModal, setShowCreateModal] = useState<boolean>(false);
|
||||
|
@ -32,7 +39,7 @@ function ProtectedAppCard({ className, hasLabel, hasCreateButton, onCreateSucces
|
|||
<>
|
||||
<div className={classNames(styles.container, className)}>
|
||||
{hasLabel && <label>{t('name')}</label>}
|
||||
<div className={styles.card}>
|
||||
<div className={classNames(styles.card, hasBorder && styles.hasBorder)}>
|
||||
<Icon className={styles.logo} />
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.name}>{t('title')}</div>
|
||||
|
|
|
@ -18,11 +18,10 @@
|
|||
|
||||
.guideLibraryContainer {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
background: var(--color-layer-1);
|
||||
border-radius: 12px;
|
||||
padding: _.unit(6) 0;
|
||||
margin: _.unit(4) 0;
|
||||
margin-top: _.unit(4);
|
||||
|
||||
.title {
|
||||
align-items: center;
|
||||
|
|
|
@ -12,7 +12,6 @@ import { isDevFeaturesEnabled, isCloud } from '@/consts/env';
|
|||
import Button from '@/ds-components/Button';
|
||||
import CardTitle from '@/ds-components/CardTitle';
|
||||
import CopyToClipboard from '@/ds-components/CopyToClipboard';
|
||||
import OverlayScrollbar from '@/ds-components/OverlayScrollbar';
|
||||
import TabNav, { TabNavItem } from '@/ds-components/TabNav';
|
||||
import Table from '@/ds-components/Table';
|
||||
import useApplicationsUsage from '@/hooks/use-applications-usage';
|
||||
|
@ -124,14 +123,14 @@ function Applications({ tab }: Props) {
|
|||
)}
|
||||
|
||||
{!isLoading && !applications?.length && (
|
||||
<OverlayScrollbar className={styles.guideLibraryContainer}>
|
||||
<div className={styles.guideLibraryContainer}>
|
||||
<CardTitle
|
||||
className={styles.title}
|
||||
title="guide.app.select_framework_or_tutorial"
|
||||
subtitle="guide.app.modal_subtitle"
|
||||
/>
|
||||
<GuideLibrary hasCardBorder hasCardButton className={styles.library} />
|
||||
</OverlayScrollbar>
|
||||
</div>
|
||||
)}
|
||||
{(isLoading || !!applications?.length) && (
|
||||
<Table
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: _.unit(6);
|
||||
height: 100%;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.headline {
|
||||
|
|
|
@ -18,12 +18,12 @@ export const applicationTypeI18nKey = Object.freeze({
|
|||
* plus the "featured" category.
|
||||
*/
|
||||
export const allAppGuideCategories = Object.freeze([
|
||||
'Protected',
|
||||
'featured',
|
||||
'Traditional',
|
||||
'SPA',
|
||||
'Native',
|
||||
'MachineToMachine',
|
||||
'Protected',
|
||||
thirdPartyAppCategory,
|
||||
] as const);
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue