From aee90d0a6f40c25c89f68bab3cb165b91959217e Mon Sep 17 00:00:00 2001
From: Charles Zhao <charleszhao@silverhand.io>
Date: Fri, 26 Jan 2024 09:39:44 +0800
Subject: [PATCH] refactor(console): improve app creation page empty state
 layout (#5310)

---
 .../EmptyDataPlaceholder/index.module.scss    |  8 ++++
 .../components/EmptyDataPlaceholder/index.tsx |  2 +
 .../components/GuideLibrary/index.module.scss | 10 ++--
 .../components/GuideLibrary/index.tsx         | 46 ++++++++++++-------
 .../components/GuideLibraryModal/index.tsx    |  2 +-
 .../ProtectedAppCard/index.module.scss        |  4 ++
 .../components/ProtectedAppCard/index.tsx     | 11 ++++-
 .../src/pages/Applications/index.module.scss  |  3 +-
 .../console/src/pages/Applications/index.tsx  |  5 +-
 .../console/src/scss/page-layout.module.scss  |  2 +-
 packages/console/src/types/applications.ts    |  2 +-
 11 files changed, 66 insertions(+), 29 deletions(-)

diff --git a/packages/console/src/components/EmptyDataPlaceholder/index.module.scss b/packages/console/src/components/EmptyDataPlaceholder/index.module.scss
index eecfa7350..c27b7122e 100644
--- a/packages/console/src/components/EmptyDataPlaceholder/index.module.scss
+++ b/packages/console/src/components/EmptyDataPlaceholder/index.module.scss
@@ -39,4 +39,12 @@
       height: 128px;
     }
   }
+
+  .topSpace {
+    flex: 2;
+  }
+
+  .bottomSpace {
+    flex: 3;
+  }
 }
diff --git a/packages/console/src/components/EmptyDataPlaceholder/index.tsx b/packages/console/src/components/EmptyDataPlaceholder/index.tsx
index 21a1b932f..8ce743d1a 100644
--- a/packages/console/src/components/EmptyDataPlaceholder/index.tsx
+++ b/packages/console/src/components/EmptyDataPlaceholder/index.tsx
@@ -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>
   );
 }
diff --git a/packages/console/src/pages/Applications/components/GuideLibrary/index.module.scss b/packages/console/src/pages/Applications/components/GuideLibrary/index.module.scss
index bc3bf17a2..8472cd136 100644
--- a/packages/console/src/pages/Applications/components/GuideLibrary/index.module.scss
+++ b/packages/console/src/pages/Applications/components/GuideLibrary/index.module.scss
@@ -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) {
diff --git a/packages/console/src/pages/Applications/components/GuideLibrary/index.tsx b/packages/console/src/pages/Applications/components/GuideLibrary/index.tsx
index daeb6ded6..c6438bccf 100644
--- a/packages/console/src/pages/Applications/components/GuideLibrary/index.tsx
+++ b/packages/console/src/pages/Applications/components/GuideLibrary/index.tsx
@@ -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 && (
diff --git a/packages/console/src/pages/Applications/components/GuideLibraryModal/index.tsx b/packages/console/src/pages/Applications/components/GuideLibraryModal/index.tsx
index db3a336bb..91dd7a338 100644
--- a/packages/console/src/pages/Applications/components/GuideLibraryModal/index.tsx
+++ b/packages/console/src/pages/Applications/components/GuideLibraryModal/index.tsx
@@ -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"
diff --git a/packages/console/src/pages/Applications/components/ProtectedAppCard/index.module.scss b/packages/console/src/pages/Applications/components/ProtectedAppCard/index.module.scss
index d58d144e9..eee6e1e79 100644
--- a/packages/console/src/pages/Applications/components/ProtectedAppCard/index.module.scss
+++ b/packages/console/src/pages/Applications/components/ProtectedAppCard/index.module.scss
@@ -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;
diff --git a/packages/console/src/pages/Applications/components/ProtectedAppCard/index.tsx b/packages/console/src/pages/Applications/components/ProtectedAppCard/index.tsx
index 5ea73c21c..ee42262cb 100644
--- a/packages/console/src/pages/Applications/components/ProtectedAppCard/index.tsx
+++ b/packages/console/src/pages/Applications/components/ProtectedAppCard/index.tsx
@@ -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>
diff --git a/packages/console/src/pages/Applications/index.module.scss b/packages/console/src/pages/Applications/index.module.scss
index c12f30ce1..e34e24b15 100644
--- a/packages/console/src/pages/Applications/index.module.scss
+++ b/packages/console/src/pages/Applications/index.module.scss
@@ -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;
diff --git a/packages/console/src/pages/Applications/index.tsx b/packages/console/src/pages/Applications/index.tsx
index e5aa53cf8..b8eaab22b 100644
--- a/packages/console/src/pages/Applications/index.tsx
+++ b/packages/console/src/pages/Applications/index.tsx
@@ -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
diff --git a/packages/console/src/scss/page-layout.module.scss b/packages/console/src/scss/page-layout.module.scss
index 4b519aac9..be77e0c1f 100644
--- a/packages/console/src/scss/page-layout.module.scss
+++ b/packages/console/src/scss/page-layout.module.scss
@@ -5,7 +5,7 @@
   display: flex;
   flex-direction: column;
   padding-bottom: _.unit(6);
-  height: 100%;
+  min-height: 100%;
 }
 
 .headline {
diff --git a/packages/console/src/types/applications.ts b/packages/console/src/types/applications.ts
index 7fd745200..27774ad7b 100644
--- a/packages/console/src/types/applications.ts
+++ b/packages/console/src/types/applications.ts
@@ -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);