0
Fork 0
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:
Charles Zhao 2024-01-26 09:39:44 +08:00 committed by GitHub
parent 47d1515d8d
commit aee90d0a6f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 66 additions and 29 deletions

View file

@ -39,4 +39,12 @@
height: 128px;
}
}
.topSpace {
flex: 2;
}
.bottomSpace {
flex: 3;
}
}

View file

@ -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>
);
}

View file

@ -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) {

View file

@ -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 && (

View file

@ -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"

View file

@ -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;

View file

@ -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>

View file

@ -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;

View file

@ -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

View file

@ -5,7 +5,7 @@
display: flex;
flex-direction: column;
padding-bottom: _.unit(6);
height: 100%;
min-height: 100%;
}
.headline {

View file

@ -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);