From 47ce579f3975e124f361461e93e9916aa3f74849 Mon Sep 17 00:00:00 2001 From: aliang <1098486429@qq.com> Date: Wed, 20 Mar 2024 14:52:39 +0800 Subject: [PATCH] refactor(ui): Optimize LoadingWrapper experience (#1692) * add debounce in loading-wrapper * table * [autofix.ci] apply automated fixes * update * update * [autofix.ci] apply automated fixes * general form --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> --- .../git/components/repository-table.tsx | 133 +++++----- .../sso/components/oauth-credential-list.tsx | 18 +- .../general/components/network-form.tsx | 14 +- .../general/components/security-form.tsx | 11 +- .../team/components/invitation-table.tsx | 75 +++--- .../settings/team/components/user-table.tsx | 247 +++++++++--------- ee/tabby-ui/components/loading-wrapper.tsx | 11 +- ee/tabby-ui/components/skeleton.tsx | 18 +- ee/tabby-ui/lib/hooks/use-debounce.ts | 4 +- ee/tabby-ui/lib/hooks/use-network-setting.tsx | 4 +- 10 files changed, 285 insertions(+), 250 deletions(-) diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/git/components/repository-table.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/git/components/repository-table.tsx index a12d903a923f..492522aea745 100644 --- a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/git/components/repository-table.tsx +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/git/components/repository-table.tsx @@ -10,7 +10,7 @@ import { RepositoriesQueryVariables, RepositoryEdge } from '@/lib/gql/generates/graphql' -import { useIsQueryInitialized, useMutation } from '@/lib/tabby/gql' +import { useMutation } from '@/lib/tabby/gql' import { listRepositories } from '@/lib/tabby/query' import { Button } from '@/components/ui/button' import { IconTrash } from '@/components/ui/icons' @@ -29,7 +29,7 @@ import { TableHeader, TableRow } from '@/components/ui/table' -import { ListSkeleton } from '@/components/skeleton' +import LoadingWrapper from '@/components/loading-wrapper' const deleteRepositoryMutation = graphql(/* GraphQL */ ` mutation deleteRepository($id: ID!) { @@ -40,11 +40,10 @@ const deleteRepositoryMutation = graphql(/* GraphQL */ ` const PAGE_SIZE = DEFAULT_PAGE_SIZE export default function RepositoryTable() { const client = useClient() - const [{ data, error, fetching, stale }] = useQuery({ + const [{ data, fetching }] = useQuery({ query: listRepositories, variables: { first: PAGE_SIZE } }) - const [initialized] = useIsQueryInitialized({ data, error, stale }) const [currentPage, setCurrentPage] = React.useState(1) const edges = data?.repositories?.edges @@ -115,75 +114,65 @@ export default function RepositoryTable() { }, [pageNum, currentPage]) return ( -
- {initialized ? ( - <> - - - - Name - Git URL - - - - - {!currentPageRepos?.length && currentPage === 1 ? ( - - - No Data - - - ) : ( - <> - {currentPageRepos?.map(x => { - return ( - - - {x.node.name} - - - {x.node.gitUrl} - - -
- -
-
-
- ) - })} - - )} -
-
- {showPagination && ( - - - - - - - - - - + + + + + Name + Git URL + + + + + {!currentPageRepos?.length && currentPage === 1 ? ( + + + No Data + + + ) : ( + <> + {currentPageRepos?.map(x => { + return ( + + {x.node.name} + {x.node.gitUrl} + +
+ +
+
+
+ ) + })} + )} - - ) : ( - +
+
+ {showPagination && ( + + + + + + + + + + )} -
+ ) } diff --git a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/oauth-credential-list.tsx b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/oauth-credential-list.tsx index 820d4bdbe9d3..e09e7a18b22b 100644 --- a/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/oauth-credential-list.tsx +++ b/ee/tabby-ui/app/(dashboard)/settings/(integrations)/sso/components/oauth-credential-list.tsx @@ -16,6 +16,7 @@ import { Button, buttonVariants } from '@/components/ui/button' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Skeleton } from '@/components/ui/skeleton' import { LicenseGuard } from '@/components/license-guard' +import LoadingWrapper from '@/components/loading-wrapper' import { PROVIDER_METAS } from './constant' import { SSOHeader } from './sso-header' @@ -64,12 +65,15 @@ const OAuthCredentialList = () => { return (
- {isLoading ? ( -
- - -
- ) : ( + + + +
+ } + >
No Data
@@ -81,7 +85,7 @@ const OAuthCredentialList = () => {
- )} + ) } diff --git a/ee/tabby-ui/app/(dashboard)/settings/general/components/network-form.tsx b/ee/tabby-ui/app/(dashboard)/settings/general/components/network-form.tsx index 5fe54da59da8..95ba4a3c8ec8 100644 --- a/ee/tabby-ui/app/(dashboard)/settings/general/components/network-form.tsx +++ b/ee/tabby-ui/app/(dashboard)/settings/general/components/network-form.tsx @@ -21,6 +21,7 @@ import { FormMessage } from '@/components/ui/form' import { Input } from '@/components/ui/input' +import LoadingWrapper from '@/components/loading-wrapper' import { FormSkeleton } from '@/components/skeleton' const updateNetworkSettingMutation = graphql(/* GraphQL */ ` @@ -114,9 +115,14 @@ export const GeneralNetworkForm = () => { reexecuteQuery() } - return data && !stale ? ( - - ) : ( - + return ( +
+ }> + + +
) } diff --git a/ee/tabby-ui/app/(dashboard)/settings/general/components/security-form.tsx b/ee/tabby-ui/app/(dashboard)/settings/general/components/security-form.tsx index e822d7103e4f..1a5ad47e8462 100644 --- a/ee/tabby-ui/app/(dashboard)/settings/general/components/security-form.tsx +++ b/ee/tabby-ui/app/(dashboard)/settings/general/components/security-form.tsx @@ -26,6 +26,7 @@ import { import { IconTrash } from '@/components/ui/icons' import { Input } from '@/components/ui/input' import { LicenseGuard } from '@/components/license-guard' +import LoadingWrapper from '@/components/loading-wrapper' import { FormSkeleton } from '@/components/skeleton' const updateSecuritySettingMutation = graphql(/* GraphQL */ ` @@ -227,7 +228,7 @@ function buildListValuesFromField(fieldListValue?: Array<{ value: string }>) { } export const GeneralSecurityForm = () => { - const [{ data, stale }, reexecuteQuery] = useQuery({ + const [{ data, stale, fetching }, reexecuteQuery] = useQuery({ query: securitySetting }) const onSuccess = () => { @@ -241,9 +242,9 @@ export const GeneralSecurityForm = () => { ) } - return data && !stale ? ( - - ) : ( - + return ( + }> + + ) } diff --git a/ee/tabby-ui/app/(dashboard)/settings/team/components/invitation-table.tsx b/ee/tabby-ui/app/(dashboard)/settings/team/components/invitation-table.tsx index fea134592a2d..bfc240c79dd1 100644 --- a/ee/tabby-ui/app/(dashboard)/settings/team/components/invitation-table.tsx +++ b/ee/tabby-ui/app/(dashboard)/settings/team/components/invitation-table.tsx @@ -32,6 +32,7 @@ import { TableRow } from '@/components/ui/table' import { CopyButton } from '@/components/copy-button' +import LoadingWrapper from '@/components/loading-wrapper' import CreateInvitationForm from './create-invitation-form' @@ -148,40 +149,46 @@ export default function InvitationTable() { return (
- - {!!currentPageInvits?.length && ( - - - Invitee - Created - - - - )} - - {currentPageInvits?.map(x => { - const link = `${externalUrl}/auth/signup?invitationCode=${x.node.code}` - return ( - - {x.node.email} - {moment.utc(x.node.createdAt).fromNow()} - -
- - -
-
-
- ) - })} -
-
+
+ + + {!!currentPageInvits?.length && ( + + + Invitee + Created + + + + )} + + {currentPageInvits?.map(x => { + const link = `${externalUrl}/auth/signup?invitationCode=${x.node.code}` + return ( + + {x.node.email} + + {moment.utc(x.node.createdAt).fromNow()} + + +
+ + +
+
+
+ ) + })} +
+
+
+
{(hasNextPage || hasPrevPage) && ( diff --git a/ee/tabby-ui/app/(dashboard)/settings/team/components/user-table.tsx b/ee/tabby-ui/app/(dashboard)/settings/team/components/user-table.tsx index d447d1f1b2c8..a4c230f18510 100644 --- a/ee/tabby-ui/app/(dashboard)/settings/team/components/user-table.tsx +++ b/ee/tabby-ui/app/(dashboard)/settings/team/components/user-table.tsx @@ -35,6 +35,7 @@ import { TableHeader, TableRow } from '@/components/ui/table' +import LoadingWrapper from '@/components/loading-wrapper' import { UpdateUserRoleDialog } from './user-role-dialog' @@ -76,7 +77,7 @@ export default function UsersTable() { const [queryVariables, setQueryVariables] = React.useState< QueryVariables >({ first: PAGE_SIZE }) - const [{ data, error }, reexecuteQuery] = useQuery({ + const [{ data, error, fetching }, reexecuteQuery] = useQuery({ query: listUsers, variables: queryVariables }) @@ -131,125 +132,135 @@ export default function UsersTable() { ) return ( - !!users?.edges?.length && ( - <> - - - - Email - Joined - Status - Level - - - - - {users.edges.map(x => { - const showOperation = - !x.node.isOwner && me?.me && x.node.id !== me.me.id - - return ( - - {x.node.email} - - {moment.utc(x.node.createdAt).fromNow()} - - - {x.node.active ? ( - Active - ) : ( - Inactive - )} - - - {makeBadge(x.node)} - - - {showOperation && ( - - - - - - {!!x.node.active && ( - onUpdateUserRole(x.node)} - className="cursor-pointer" - > - - {x.node.isAdmin - ? 'Downgrade to member' - : 'Upgrade to admin'} - - - )} - {!!x.node.active && ( - onUpdateUserActive(x.node, false)} - className="cursor-pointer" - > - Deactivate - - )} - {!x.node.active && ( - onUpdateUserActive(x.node, true)} - className="cursor-pointer" - > - Activate - - )} - - - )} - + <> + + {!!users?.edges?.length && ( + <> +
+ + + Email + Joined + Status + Level + - ) - })} - -
- {(pageInfo?.hasNextPage || pageInfo?.hasPreviousPage) && ( - - - - - setQueryVariables({ - last: PAGE_SIZE, - before: pageInfo?.startCursor - }) - } - /> - - - - setQueryVariables({ - first: PAGE_SIZE, - after: pageInfo?.endCursor - }) - } - /> - - - + + + {users.edges.map(x => { + const showOperation = + !x.node.isOwner && me?.me && x.node.id !== me.me.id + + return ( + + {x.node.email} + + {moment.utc(x.node.createdAt).fromNow()} + + + {x.node.active ? ( + Active + ) : ( + Inactive + )} + + + {makeBadge(x.node)} + + + {showOperation && ( + + + + + + {!!x.node.active && ( + onUpdateUserRole(x.node)} + className="cursor-pointer" + > + + {x.node.isAdmin + ? 'Downgrade to member' + : 'Upgrade to admin'} + + + )} + {!!x.node.active && ( + + onUpdateUserActive(x.node, false) + } + className="cursor-pointer" + > + Deactivate + + )} + {!x.node.active && ( + + onUpdateUserActive(x.node, true) + } + className="cursor-pointer" + > + Activate + + )} + + + )} + + + ) + })} + + + {(pageInfo?.hasNextPage || pageInfo?.hasPreviousPage) && ( + + + + + setQueryVariables({ + last: PAGE_SIZE, + before: pageInfo?.startCursor + }) + } + /> + + + + setQueryVariables({ + first: PAGE_SIZE, + after: pageInfo?.endCursor + }) + } + /> + + + + )} + )} + - { - reexecuteQuery() - setUpdateRoleVisible(false) - }} - user={currentUser} - isPromote={isPromote} - open={updateRoleVisible} - onOpenChange={setUpdateRoleVisible} - /> - - ) + { + reexecuteQuery() + setUpdateRoleVisible(false) + }} + user={currentUser} + isPromote={isPromote} + open={updateRoleVisible} + onOpenChange={setUpdateRoleVisible} + /> + ) } diff --git a/ee/tabby-ui/components/loading-wrapper.tsx b/ee/tabby-ui/components/loading-wrapper.tsx index 0bf19559ae6f..7c2ac01e43f3 100644 --- a/ee/tabby-ui/components/loading-wrapper.tsx +++ b/ee/tabby-ui/components/loading-wrapper.tsx @@ -2,18 +2,25 @@ import React from 'react' +import { useDebounceValue } from '@/lib/hooks/use-debounce' + +import { ListSkeleton } from './skeleton' + interface LoadingWrapperProps { loading?: boolean children?: React.ReactNode fallback?: React.ReactNode + delay?: number } export const LoadingWrapper: React.FC = ({ loading, fallback, + delay, children }) => { const [loaded, setLoaded] = React.useState(!loading) + const [debouncedLoaded] = useDebounceValue(loaded, delay ?? 200) React.useEffect(() => { if (!loading && !loaded) { @@ -21,8 +28,8 @@ export const LoadingWrapper: React.FC = ({ } }, [loading]) - if (!loaded) { - return fallback + if (!debouncedLoaded) { + return fallback ? fallback : } else { return children } diff --git a/ee/tabby-ui/components/skeleton.tsx b/ee/tabby-ui/components/skeleton.tsx index 0c5cfc940453..e985f9c4f480 100644 --- a/ee/tabby-ui/components/skeleton.tsx +++ b/ee/tabby-ui/components/skeleton.tsx @@ -1,10 +1,17 @@ 'use client' +import { HTMLAttributes } from 'react' + +import { cn } from '@/lib/utils' + import { Skeleton } from './ui/skeleton' -export const ListSkeleton = () => { +export const ListSkeleton: React.FC> = ({ + className, + ...props +}) => { return ( -
+
@@ -13,9 +20,12 @@ export const ListSkeleton = () => { ) } -export const FormSkeleton = () => { +export const FormSkeleton: React.FC> = ({ + className, + ...props +}) => { return ( -
+
diff --git a/ee/tabby-ui/lib/hooks/use-debounce.ts b/ee/tabby-ui/lib/hooks/use-debounce.ts index 180711764893..534e55426e95 100644 --- a/ee/tabby-ui/lib/hooks/use-debounce.ts +++ b/ee/tabby-ui/lib/hooks/use-debounce.ts @@ -12,7 +12,7 @@ type noop = (...args: any[]) => any function useDebounceCallback( fn: T, - wait: number, + wait?: number, options?: DebounceSettings ) { const fnRef = useLatest(fn) @@ -39,7 +39,7 @@ function useDebounceCallback( function useDebounceValue( value: T, - wait: number, + wait?: number, options?: DebounceSettings ): [T, React.Dispatch>] { const [debouncedValue, setDebouncedValue] = React.useState(value) diff --git a/ee/tabby-ui/lib/hooks/use-network-setting.tsx b/ee/tabby-ui/lib/hooks/use-network-setting.tsx index d7fde046e265..d4097eb0fb50 100644 --- a/ee/tabby-ui/lib/hooks/use-network-setting.tsx +++ b/ee/tabby-ui/lib/hooks/use-network-setting.tsx @@ -12,8 +12,8 @@ const networkSettingQuery = graphql(/* GraphQL */ ` } `) -const useNetworkSetting = () => { - return useQuery({ query: networkSettingQuery }) +const useNetworkSetting = (options?: any) => { + return useQuery({ query: networkSettingQuery, ...options }) } const useExternalURL = () => {