From e9b9b4e90db84cdce6e1ffad3b1a0714dcb6a305 Mon Sep 17 00:00:00 2001 From: Anass Bouassaba Date: Tue, 12 Nov 2024 08:41:58 +0100 Subject: [PATCH 01/13] wip(ui): error handling in UI --- api/errorpkg/error_constants.go | 17 -- api/errorpkg/error_creators.go | 34 ++-- api/router/user_router.go | 2 +- api/service/user_service.go | 12 +- ui/src/components/common/group-selector.tsx | 143 +++++++-------- .../common/organization-selector.tsx | 33 +--- ui/src/components/common/user-selector.tsx | 173 +++++++++--------- ui/src/components/insights/index.tsx | 36 +++- .../components/insights/insights-create.tsx | 85 +++++---- .../insights-overview-artifacts.tsx | 77 ++++---- .../insights-overview-chart.tsx | 110 +++++------ .../insights-overview-entities.tsx | 127 +++++++------ .../insights-overview-settings.tsx | 132 +++++++------ 13 files changed, 501 insertions(+), 480 deletions(-) delete mode 100644 api/errorpkg/error_constants.go diff --git a/api/errorpkg/error_constants.go b/api/errorpkg/error_constants.go deleted file mode 100644 index f22749155..000000000 --- a/api/errorpkg/error_constants.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2023 Anass Bouassaba. -// -// Use of this software is governed by the Business Source License -// included in the file licenses/BSL.txt. -// -// As of the Change Date specified in that file, in accordance with -// the Business Source License, use of this software will be governed -// by the GNU Affero General Public License v3.0 only, included in the file -// licenses/AGPL.txt. - -package errorpkg - -const ( - MsgResourceNotFound = "The resource you are looking for might have been removed or temporatily unavailable." - MsgSomethingWentWrong = "Oops! something went wrong." - MsgInvalidRequest = "An invalid request was sent to the server." -) diff --git a/api/errorpkg/error_creators.go b/api/errorpkg/error_creators.go index 4289d46cb..6274f09a8 100644 --- a/api/errorpkg/error_creators.go +++ b/api/errorpkg/error_creators.go @@ -25,7 +25,7 @@ func NewGroupNotFoundError(err error) *ErrorResponse { "group_not_found", http.StatusNotFound, "Group not found.", - MsgResourceNotFound, + "Group not found.", err, ) } @@ -35,7 +35,7 @@ func NewFileNotFoundError(err error) *ErrorResponse { "file_not_found", http.StatusNotFound, "File not found.", - MsgResourceNotFound, + "File not found.", err, ) } @@ -45,7 +45,7 @@ func NewInvalidPathError(err error) *ErrorResponse { "invalid_path", http.StatusBadRequest, "Invalid path.", - MsgInvalidRequest, + "An invalid request was sent to the server.", err, ) } @@ -55,7 +55,7 @@ func NewWorkspaceNotFoundError(err error) *ErrorResponse { "workspace_not_found", http.StatusNotFound, "Workspace not found.", - MsgResourceNotFound, + "Workspace not found.", err, ) } @@ -65,7 +65,7 @@ func NewOrganizationNotFoundError(err error) *ErrorResponse { "organization_not_found", http.StatusNotFound, "Organization not found.", - MsgResourceNotFound, + "Organization not found.", err, ) } @@ -75,7 +75,7 @@ func NewTaskNotFoundError(err error) *ErrorResponse { "task_not_found", http.StatusNotFound, "Task not found.", - MsgResourceNotFound, + "Task not found.", err, ) } @@ -105,17 +105,17 @@ func NewUserNotFoundError(err error) *ErrorResponse { "user_not_found", http.StatusNotFound, "User not found.", - MsgResourceNotFound, + "User not found.", err, ) } -func NewUserPictureNotFoundError(err error) *ErrorResponse { +func NewPictureNotFoundError(err error) *ErrorResponse { return NewErrorResponse( - "user_picture_not_found", + "Picture_not_found", http.StatusNotFound, - "User picture not found.", - MsgResourceNotFound, + "Picture not found.", + "Picture not found.", err, ) } @@ -145,7 +145,7 @@ func NewInvitationNotFoundError(err error) *ErrorResponse { "invitation_not_found", http.StatusNotFound, "Invitation not found.", - MsgResourceNotFound, + "Invitation not found.", err, ) } @@ -195,7 +195,7 @@ func NewInternalServerError(err error) *ErrorResponse { "internal_server_error", http.StatusInternalServerError, "Internal server error.", - MsgSomethingWentWrong, + "Oops! something went wrong.", err, ) } @@ -277,7 +277,7 @@ func NewS3Error(message string) *ErrorResponse { "s3_error", http.StatusInternalServerError, message, - MsgSomethingWentWrong, + "Storage error occurred.", nil, ) } @@ -287,7 +287,7 @@ func NewMissingQueryParamError(param string) *ErrorResponse { "missing_query_param", http.StatusBadRequest, fmt.Sprintf("Query param '%s' is required.", param), - MsgInvalidRequest, + "An invalid request was sent to the server.", nil, ) } @@ -297,7 +297,7 @@ func NewInvalidQueryParamError(param string) *ErrorResponse { "invalid_query_param", http.StatusBadRequest, fmt.Sprintf("Query param '%s' is invalid.", param), - MsgInvalidRequest, + "An invalid request was sent to the server.", nil, ) } @@ -332,7 +332,7 @@ func NewRequestBodyValidationError(err error) *ErrorResponse { "request_validation_error", http.StatusBadRequest, fmt.Sprintf("Failed validation for fields: %s.", strings.Join(fields, ",")), - MsgInvalidRequest, + "An invalid request was sent to the server.", err, ) } diff --git a/api/router/user_router.go b/api/router/user_router.go index b8a95d168..f78ebb35c 100644 --- a/api/router/user_router.go +++ b/api/router/user_router.go @@ -211,7 +211,7 @@ func (r *UserRouter) DownloadPicture(c *fiber.Ctx) error { return err } if *ext != c.Params("extension") { - return errorpkg.NewUserPictureNotFoundError(nil) + return errorpkg.NewPictureNotFoundError(nil) } c.Set("Content-Type", *mime) c.Set("Content-Disposition", fmt.Sprintf("filename=\"picture%s\"", *ext)) diff --git a/api/service/user_service.go b/api/service/user_service.go index 6daaf88d3..f87deaf0a 100644 --- a/api/service/user_service.go +++ b/api/service/user_service.go @@ -270,13 +270,13 @@ func (svc *UserService) ExtractPicture(id string, justification ExtractPictureJu return nil, nil, nil, err } if user.GetPicture() == nil { - return nil, nil, nil, errorpkg.NewUserPictureNotFoundError(nil) + return nil, nil, nil, errorpkg.NewPictureNotFoundError(nil) } mime := helper.Base64ToMIME(*user.GetPicture()) ext := helper.Base64ToExtension(*user.GetPicture()) b, err := helper.Base64ToBytes(*user.GetPicture()) if err != nil { - return nil, nil, nil, errorpkg.NewUserPictureNotFoundError(nil) + return nil, nil, nil, errorpkg.NewPictureNotFoundError(nil) } return b, &ext, &mime, nil } @@ -290,7 +290,7 @@ func (svc *UserService) findUserForPicture(id string, justification ExtractPictu return user, nil } if justification.OrganizationID == nil && justification.GroupID == nil && justification.InvitationID == nil { - return nil, errorpkg.NewUserPictureNotFoundError(nil) + return nil, errorpkg.NewPictureNotFoundError(nil) } if justification.OrganizationID != nil { org, err := svc.orgCache.Get(*justification.OrganizationID) @@ -301,7 +301,7 @@ func (svc *UserService) findUserForPicture(id string, justification ExtractPictu return nil, err } if !slices.Contains(org.GetMembers(), id) { - return nil, errorpkg.NewUserPictureNotFoundError(nil) + return nil, errorpkg.NewPictureNotFoundError(nil) } } else if justification.GroupID != nil { group, err := svc.groupCache.Get(*justification.GroupID) @@ -312,7 +312,7 @@ func (svc *UserService) findUserForPicture(id string, justification ExtractPictu return nil, err } if !slices.Contains(group.GetMembers(), id) { - return nil, errorpkg.NewUserPictureNotFoundError(nil) + return nil, errorpkg.NewPictureNotFoundError(nil) } } else if justification.InvitationID != nil { invitation, err := svc.invitationRepo.Find(*justification.InvitationID) @@ -320,7 +320,7 @@ func (svc *UserService) findUserForPicture(id string, justification ExtractPictu return nil, err } if invitation.GetOwnerID() != id { - return nil, errorpkg.NewUserPictureNotFoundError(nil) + return nil, errorpkg.NewPictureNotFoundError(nil) } } return user, nil diff --git a/ui/src/components/common/group-selector.tsx b/ui/src/components/common/group-selector.tsx index 397098e10..22a938e75 100644 --- a/ui/src/components/common/group-selector.tsx +++ b/ui/src/components/common/group-selector.tsx @@ -59,6 +59,10 @@ const GroupSelector = ({ { query, organizationId, page, size: 5, sortOrder: SortOrder.Desc }, swrConfig(), ) + const isLoading = !list && !error + const isError = !list && error + const isEmpty = list && !error && list.totalElements === 0 + const isSuccess = list && !error && list.totalElements > 0 useEffect(() => { mutate().then() @@ -116,84 +120,77 @@ const GroupSelector = ({ query={query} onChange={handleSearchInputChange} /> - {!list && error ? ( - + {isLoading ? : null} + {isError ? : null} + {isEmpty ? ( + ) : null} - {!list && !error ? : null} - {list && !error ? ( - <> - {list.totalElements > 0 ? ( -
- - - - - - - {list.data.map((g) => ( - +
+ + + + + + {list.data.map((g) => ( + setSelected(g)} + > + + - - - ))} - -
+ + +
setSelected(g)} > -
- - -
- - - {g.name} - -
-
- {list.totalPages > 1 ? ( -
- setPage(value)} - /> -
- ) : null} + + {g.name} +
+ + + ))} + + + {list.totalPages > 1 ? ( +
+ setPage(value)} + />
- ) : ( - - )} - + ) : null} + ) : null} diff --git a/ui/src/components/common/organization-selector.tsx b/ui/src/components/common/organization-selector.tsx index 0b976e3e0..c4d9317ed 100644 --- a/ui/src/components/common/organization-selector.tsx +++ b/ui/src/components/common/organization-selector.tsx @@ -29,6 +29,7 @@ import { Pagination, SearchInput, SectionError, + SectionPlaceholder, SectionSpinner, } from '@koupr/ui' import cx from 'classnames' @@ -56,6 +57,10 @@ const OrganizationSelector = ({ onConfirm }: OrganizationSelectorProps) => { { query, page, size: 5, sortOrder: SortOrder.Desc }, swrConfig(), ) + const isLoading = !list && !error + const isError = !list && error + const isEmpty = list && !error && list.totalElements === 0 + const isSuccess = list && !error && list.totalElements > 0 useEffect(() => { mutate().then() @@ -114,32 +119,14 @@ const OrganizationSelector = ({ onConfirm }: OrganizationSelectorProps) => { query={query} onChange={handleSearchInputChange} /> - {!list && error ? ( + {isLoading ? : null} + {isError ? ( ) : null} - {!list && !error ? : null} - {list && list.totalElements === 0 ? ( -
-
- There are no organizations. -
-
+ {isEmpty ? ( + ) : null} - {list && list.totalElements && !error ? ( + {isSuccess ? (
0 useEffect(() => { mutate().then() @@ -130,98 +134,93 @@ const UserSelector = ({ query={query} onChange={handleSearchInputChange} /> - {!list && error ? ( - + {isError ? : null} + {isLoading ? : null} + {isEmpty ? ( + ) : null} - {!list && !error ? : null} - {list && !error ? ( - <> - {list.totalElements > 0 ? ( -
- - - - - - - {list.data.map((u) => ( - +
+ + + + + + {list.data.map((u) => ( + setSelected(u)} + > + + - - - ))} - -
+ + +
setSelected(u)} > -
- - -
- - - {userToString(u)} - -
-
- {list.totalPages > 1 ? ( -
- setPage(value)} - /> -
- ) : null} + + + {userToString(u)} + +
+ + + ))} + + + {list.totalPages > 1 ? ( +
+ setPage(value)} + />
- ) : ( - - )} - + ) : null} +
) : null} diff --git a/ui/src/components/insights/index.tsx b/ui/src/components/insights/index.tsx index ff024a41b..f70f1f699 100644 --- a/ui/src/components/insights/index.tsx +++ b/ui/src/components/insights/index.tsx @@ -15,6 +15,7 @@ import { ModalHeader, ModalOverlay, } from '@chakra-ui/react' +import { SectionError, SectionSpinner } from '@koupr/ui' import FileAPI from '@/client/api/file' import InsightsAPI from '@/client/api/insights' import { swrConfig } from '@/client/options' @@ -31,11 +32,18 @@ const Insights = () => { : undefined, ) const isModalOpen = useAppSelector((state) => state.ui.insights.isModalOpen) - const { data: info, mutate: mutateInfo } = InsightsAPI.useGetInfo( - id, - swrConfig(), - ) - const { data: file } = FileAPI.useGet(id, swrConfig()) + const { + data: info, + error: infoError, + mutate: mutateInfo, + } = InsightsAPI.useGetInfo(id, swrConfig()) + const { data: file, error: fileError } = FileAPI.useGet(id, swrConfig()) + const isInfoLoading = !info && !infoError + const isInfoError = !info && infoError + const isInfoSuccess = info && !infoError + const isFileLoading = !file && !fileError + const isFileError = !file && fileError + const isFileSuccess = file && !fileError useEffect(() => { if (mutateInfo) { @@ -49,10 +57,6 @@ const Insights = () => { } }, [file]) - if (!info) { - return null - } - return ( { Insights - {info.isAvailable ? : } + {isFileLoading ? : null} + {isFileError ? : null} + {isFileSuccess ? ( + <> + {isInfoLoading ? : null} + {isInfoError ? : null} + {isInfoSuccess ? ( + <> + {info?.isAvailable ? : } + + ) : null} + + ) : null} ) diff --git a/ui/src/components/insights/insights-create.tsx b/ui/src/components/insights/insights-create.tsx index 879d08ad0..cd1162c92 100644 --- a/ui/src/components/insights/insights-create.tsx +++ b/ui/src/components/insights/insights-create.tsx @@ -9,7 +9,7 @@ // licenses/AGPL.txt. import { useCallback, useMemo, useState } from 'react' import { Button, ModalBody, ModalFooter } from '@chakra-ui/react' -import { Select } from '@koupr/ui' +import { SectionError, SectionSpinner, Select } from '@koupr/ui' import { OptionBase, SingleValue } from 'chakra-react-select' import cx from 'classnames' import FileAPI from '@/client/api/file' @@ -35,8 +35,9 @@ const InsightsCreate = () => { const mutateTasks = useAppSelector((state) => state.ui.tasks.mutateList) const mutateInfo = useAppSelector((state) => state.ui.insights.mutateInfo) const [language, setLanguage] = useState() - const { data: languages } = InsightsAPI.useGetLanguages(swrConfig()) - const { data: file } = FileAPI.useGet(id, swrConfig()) + const { data: languages, error: languagesError } = + InsightsAPI.useGetLanguages(swrConfig()) + const { data: file, error: fileError } = FileAPI.useGet(id, swrConfig()) const existingLanguage = useMemo(() => { if (file && languages && file.snapshot?.language) { const value = file.snapshot.language @@ -48,6 +49,12 @@ const InsightsCreate = () => { } } }, [file, languages]) + const isFileLoading = !file && !fileError + const isFileError = !file && fileError + const isFileSuccess = file && !fileError + const isLanguagesLoading = !languages && !languagesError + const isLanguagesError = !languages && languagesError + const isLanguagesSuccess = languages && !languagesError const handleCreate = useCallback(async () => { if (id && language) { @@ -68,42 +75,48 @@ const InsightsCreate = () => { [languages], ) - if (!id || !file || !languages) { - return null - } - return ( <> -
-

- Select the language to use for collecting insights. During the - process, text will be extracted using OCR (optical character - recognition), and entities will be scanned using NER (named entity - recognition). -

- {languages ? ( - - className={cx('w-full')} - defaultValue={existingLanguage} - options={languages.map((language) => ({ - value: language.id, - label: language.name, - }))} - placeholder="Select Language" - selectedOptionStyle="check" - onChange={handleLanguageChange} - /> - ) : null} -
+ {isFileLoading ? : null} + {isFileError ? : null} + {isFileSuccess ? ( + <> + {isLanguagesLoading ? : null} + {isLanguagesError ? ( + + ) : null} + {isLanguagesSuccess ? ( +
+

+ Select the language to use for collecting insights. During the + process, text will be extracted using OCR (optical character + recognition), and entities will be scanned using NER (named + entity recognition). +

+ + className={cx('w-full')} + defaultValue={existingLanguage} + options={languages.map((language) => ({ + value: language.id, + label: language.name, + }))} + placeholder="Select Language" + selectedOptionStyle="check" + onChange={handleLanguageChange} + /> +
+ ) : null} + + ) : null}
diff --git a/ui/src/components/insights/insights-overview/insights-overview-artifacts.tsx b/ui/src/components/insights/insights-overview/insights-overview-artifacts.tsx index 6655f1407..0cd417706 100644 --- a/ui/src/components/insights/insights-overview/insights-overview-artifacts.tsx +++ b/ui/src/components/insights/insights-overview/insights-overview-artifacts.tsx @@ -8,7 +8,7 @@ // by the GNU Affero General Public License v3.0 only, included in the file // licenses/AGPL.txt. import { Button } from '@chakra-ui/react' -import { IconOpenInNew } from '@koupr/ui' +import { IconOpenInNew, SectionError, SectionSpinner } from '@koupr/ui' import cx from 'classnames' import FileAPI from '@/client/api/file' import { swrConfig } from '@/client/options' @@ -21,48 +21,53 @@ const InsightsOverviewArtifacts = () => { ? state.ui.files.selection[0] : undefined, ) - const { data: file } = FileAPI.useGet(id, swrConfig()) + const { data: file, error } = FileAPI.useGet(id, swrConfig()) const searchParams = new URLSearchParams({ access_token: getAccessTokenOrRedirect(), }) - - if (!id || !file) { - return null - } + const isLoading = !file && !error + const isError = !file && error + const isSuccess = file && !error return ( -
- {file.snapshot?.text ? ( - - ) : null} - {file.snapshot?.ocr ? ( - + {file.snapshot?.text ? ( + + ) : null} + {file.snapshot?.ocr ? ( + + ) : null} +
) : null} -
+ ) } diff --git a/ui/src/components/insights/insights-overview/insights-overview-chart.tsx b/ui/src/components/insights/insights-overview/insights-overview-chart.tsx index ec5a3f3b4..63749291c 100644 --- a/ui/src/components/insights/insights-overview/insights-overview-chart.tsx +++ b/ui/src/components/insights/insights-overview/insights-overview-chart.tsx @@ -7,8 +7,8 @@ // the Business Source License, use of this software will be governed // by the GNU Affero General Public License v3.0 only, included in the file // licenses/AGPL.txt. -import { useMemo } from 'react' import { useColorMode } from '@chakra-ui/react' +import { SectionError, SectionPlaceholder, SectionSpinner } from '@koupr/ui' import { ResponsivePie } from '@nivo/pie' import cx from 'classnames' import InsightsAPI, { SortBy, SortOrder } from '@/client/api/insights' @@ -22,64 +22,68 @@ const InsightsOverviewChart = () => { ? state.ui.files.selection[0] : undefined, ) - const { data: entities } = InsightsAPI.useListEntities( + const { data: list, error } = InsightsAPI.useListEntities( id, { page: 1, size: 5, sortBy: SortBy.Frequency, sortOrder: SortOrder.Desc }, swrConfig(), ) - const data = useMemo(() => { - if (entities && entities.size >= 5) { - return entities.data.map((entity) => ({ - id: entity.text, - label: entity.text, - value: entity.frequency, - })) - } - }, [entities]) + const isLoading = !list && !error + const isError = !list && error + const isEmpty = list && !error && list.totalElements < 5 + const isSuccess = list && !error && list.totalElements >= 5 return ( -
- {data ? ( - - ) : ( -

Not enough data to render the chart.

- )} -
+ <> + {isLoading ? : null} + {isError ? : null} + {isEmpty ? ( + + ) : null} + {isSuccess ? ( +
+ ({ + id: entity.text, + label: entity.text, + value: entity.frequency, + }))} + colors={{ scheme: colorMode === 'light' ? 'greys' : 'nivo' }} + margin={{ top: 40, right: 40, bottom: 40, left: 40 }} + innerRadius={0.6} + padAngle={3} + cornerRadius={3} + activeOuterRadiusOffset={8} + borderWidth={2} + borderColor={{ + from: 'color', + modifiers: [['darker', 0.2]], + }} + arcLinkLabelsSkipAngle={10} + arcLinkLabelsTextColor={ + colorMode === 'light' ? 'rgb(26, 32, 44)' : 'white' + } + arcLinkLabelsThickness={2} + arcLinkLabelsColor={{ + from: 'color', + modifiers: [['darker', 0.2]], + }} + arcLabelsSkipAngle={10} + arcLabelsTextColor={{ + from: 'color', + modifiers: [['darker', 2]], + }} + /> +
+ ) : null} + ) } diff --git a/ui/src/components/insights/insights-overview/insights-overview-entities.tsx b/ui/src/components/insights/insights-overview/insights-overview-entities.tsx index 651c458c5..a5e91a77a 100644 --- a/ui/src/components/insights/insights-overview/insights-overview-entities.tsx +++ b/ui/src/components/insights/insights-overview/insights-overview-entities.tsx @@ -45,6 +45,10 @@ const InsightsOverviewEntities = () => { }, query ? undefined : swrConfig(), ) + const isLoading = !list && !error + const isError = !list && error + const isEmpty = list && !error && list.totalElements === 0 + const isSuccess = list && !error && list.totalElements > 0 useEffect(() => { mutate().then() @@ -68,71 +72,66 @@ const InsightsOverviewEntities = () => { onValue={handleSearchInputValue} onClear={handleSearchInputClear} /> - {!list && error ? : null} - {!list && !error ? : null} - {list && !error ? ( - <> - {list.totalElements > 0 ? ( -
- - - - - - - {list.data.map((entity, index) => ( - - - - ))} - -
-
- {entity.text} - {getEntityDescription(entity.label) ? ( - - - {entity.label} - - - ) : ( - - {entity.label} - - )} - {entity.frequency} -
-
- {list.totalPages > 1 ? ( -
- setPage(value)} - /> -
- ) : null} -
- ) : ( - + {isLoading ? : null} + {isError ? : null} + {isEmpty ? : null} + {isSuccess ? ( +
+ > + + + + + + + {list.data.map((entity, index) => ( + + + + ))} + +
+
+ {entity.text} + {getEntityDescription(entity.label) ? ( + + + {entity.label} + + + ) : ( + + {entity.label} + + )} + {entity.frequency} +
+
+ {list.totalPages > 1 ? ( +
+ setPage(value)} + /> +
+ ) : null} +
) : null} ) diff --git a/ui/src/components/insights/insights-overview/insights-overview-settings.tsx b/ui/src/components/insights/insights-overview/insights-overview-settings.tsx index acacac404..cf7874b38 100644 --- a/ui/src/components/insights/insights-overview/insights-overview-settings.tsx +++ b/ui/src/components/insights/insights-overview/insights-overview-settings.tsx @@ -9,7 +9,7 @@ // licenses/AGPL.txt. import { useCallback, useMemo } from 'react' import { Button, Card, CardBody, CardFooter, Text } from '@chakra-ui/react' -import { IconBolt, IconDelete } from '@koupr/ui' +import { IconBolt, IconDelete, SectionError, SectionSpinner } from '@koupr/ui' import cx from 'classnames' import FileAPI from '@/client/api/file' import InsightsAPI from '@/client/api/insights' @@ -32,11 +32,37 @@ const InsightsOverviewSettings = () => { ) const mutateFiles = useAppSelector((state) => state.ui.files.mutate) const mutateTaskCount = useAppSelector((state) => state.ui.tasks.mutateCount) - const { data: info, mutate: mutateInfo } = InsightsAPI.useGetInfo( - id, - swrConfig(), - ) - const { data: file, mutate: mutateFile } = FileAPI.useGet(id, swrConfig()) + const { + data: info, + error: infoError, + mutate: mutateInfo, + } = InsightsAPI.useGetInfo(id, swrConfig()) + const { + data: file, + error: fileError, + mutate: mutateFile, + } = FileAPI.useGet(id, swrConfig()) + const canCollect = useMemo(() => { + return !!( + !file?.snapshot?.task?.isPending && + info?.isOutdated && + geEditorPermission(file?.permission ?? NONE_PERMISSION) + ) + }, [info, file]) + + const canDelete = useMemo(() => { + return ( + !file?.snapshot?.task?.isPending && + !info?.isOutdated && + geOwnerPermission(file?.permission ?? NONE_PERMISSION) + ) + }, [info, file]) + const isFileLoading = !file && !fileError + const isFileError = !file && fileError + const isFileSuccess = file && !fileError + const isInfoLoading = !info && !infoError + const isInfoError = !info && infoError + const isInfoSuccess = info && !infoError const handleUpdate = useCallback(async () => { if (id) { @@ -60,58 +86,50 @@ const InsightsOverviewSettings = () => { } }, [id, mutateFile, mutateFiles, mutateTaskCount, mutateInfo, dispatch]) - const canCollect = useMemo(() => { - return !!( - !file?.snapshot?.task?.isPending && - info?.isOutdated && - geEditorPermission(file?.permission ?? NONE_PERMISSION) - ) - }, [info, file]) - - const canDelete = useMemo(() => { - return ( - !file?.snapshot?.task?.isPending && - !info?.isOutdated && - geOwnerPermission(file?.permission ?? NONE_PERMISSION) - ) - }, [info, file]) - - if (!file || !info) { - return null - } - return ( -
- - - Collect insights for the active snapshot. - - - - - - - - Delete insights from the active snapshot. - - - - - -
+ <> + {isFileLoading ? : null} + {isFileError ? : null} + {isFileSuccess ? ( + <> + {isInfoLoading ? : null} + {isInfoError ? : null} + {isInfoSuccess ? ( +
+ + + Collect insights for the active snapshot. + + + + + + + + Delete insights from the active snapshot. + + + + + +
+ ) : null} + + ) : null} + ) } From f247afc07adba252310ccfdd276bf59771d09d75 Mon Sep 17 00:00:00 2001 From: Anass Bouassaba Date: Tue, 12 Nov 2024 19:30:07 +0100 Subject: [PATCH 02/13] wip(ui): error handling for Mosaic --- ui/src/components/mosaic/index.tsx | 34 ++++-- .../mosaic/mosaic-overview/index.tsx | 82 +++++++------- .../mosaic-overview-artifacts.tsx | 55 +++++----- .../mosaic-overview-settings.tsx | 101 +++++++++++------- 4 files changed, 157 insertions(+), 115 deletions(-) diff --git a/ui/src/components/mosaic/index.tsx b/ui/src/components/mosaic/index.tsx index 36c0d2f4c..5793eb664 100644 --- a/ui/src/components/mosaic/index.tsx +++ b/ui/src/components/mosaic/index.tsx @@ -15,6 +15,7 @@ import { ModalHeader, ModalOverlay, } from '@chakra-ui/react' +import { SectionError, SectionSpinner } from '@koupr/ui' import FileAPI from '@/client/api/file' import MosaicAPI from '@/client/api/mosaic' import { swrConfig } from '@/client/options' @@ -32,11 +33,18 @@ const Mosaic = () => { : undefined, ) const isModalOpen = useAppSelector((state) => state.ui.mosaic.isModalOpen) - const { data: info, mutate: mutateInfo } = MosaicAPI.useGetInfo( - id, - swrConfig(), - ) - const { data: file } = FileAPI.useGet(id, swrConfig()) + const { + data: info, + error: infoError, + mutate: mutateInfo, + } = MosaicAPI.useGetInfo(id, swrConfig()) + const { data: file, error: fileError } = FileAPI.useGet(id, swrConfig()) + const isFileLoading = !file && !fileError + const isFileError = !file && fileError + const isFileSuccess = file && !fileError + const isInfoLoading = !info && !infoError + const isInfoError = !info && infoError + const isInfoSuccess = info && !infoError useEffect(() => { if (file?.snapshot?.task?.isPending) { @@ -50,10 +58,6 @@ const Mosaic = () => { } }, [mutateInfo]) - if (!info) { - return null - } - return ( { Mosaic - {info.isAvailable ? : } + {isFileLoading ? : null} + {isFileError ? : null} + {isFileSuccess ? ( + <> + {isInfoLoading ? : null} + {isInfoError ? : null} + {isInfoSuccess ? ( + <>{info.isAvailable ? : } + ) : null} + + ) : null} ) diff --git a/ui/src/components/mosaic/mosaic-overview/index.tsx b/ui/src/components/mosaic/mosaic-overview/index.tsx index 9a26d1d63..59d090dd1 100644 --- a/ui/src/components/mosaic/mosaic-overview/index.tsx +++ b/ui/src/components/mosaic/mosaic-overview/index.tsx @@ -21,6 +21,7 @@ import { TabPanels, Tabs, } from '@chakra-ui/react' +import { SectionError, SectionSpinner } from '@koupr/ui' import cx from 'classnames' import MosaicAPI from '@/client/api/mosaic' import { swrConfig } from '@/client/options' @@ -35,49 +36,52 @@ const MosaicOverview = () => { : undefined, ) const [isWarningVisible, setIsWarningVisible] = useState(true) - const { data: info } = MosaicAPI.useGetInfo(id, swrConfig()) - - if (!info) { - return null - } + const { data: info, error } = MosaicAPI.useGetInfo(id, swrConfig()) + const isLoading = !info && !error + const isError = !info && error + const isSuccess = info && !error return ( <> -
- {info.isOutdated && isWarningVisible ? ( - - - - - This mosaic comes from an older snapshot. You can create a new - one for the active snapshot from the settings. - - - setIsWarningVisible(false)} - /> - - ) : null} - - - Artifacts - Settings - - - - - - - - - - -
+ {isLoading ? : null} + {isError ? : null} + {isSuccess ? ( +
+ {info.isOutdated && isWarningVisible ? ( + + + + + This mosaic comes from an older snapshot. You can create a + new one for the active snapshot from the settings. + + + setIsWarningVisible(false)} + /> + + ) : null} + + + Artifacts + Settings + + + + + + + + + + +
+ ) : null}
) diff --git a/ui/src/components/mosaic/mosaic-overview/mosaic-overview-artifacts.tsx b/ui/src/components/mosaic/mosaic-overview/mosaic-overview-artifacts.tsx index a3b733ac4..66646150c 100644 --- a/ui/src/components/mosaic/mosaic-overview/mosaic-overview-artifacts.tsx +++ b/ui/src/components/mosaic/mosaic-overview/mosaic-overview-artifacts.tsx @@ -8,7 +8,7 @@ // by the GNU Affero General Public License v3.0 only, included in the file // licenses/AGPL.txt. import { Button } from '@chakra-ui/react' -import { IconOpenInNew } from '@koupr/ui' +import { IconOpenInNew, SectionError, SectionSpinner } from '@koupr/ui' import cx from 'classnames' import FileAPI from '@/client/api/file' import { swrConfig } from '@/client/options' @@ -20,32 +20,37 @@ const MosaicOverviewArtifacts = () => { ? state.ui.files.selection[0] : undefined, ) - const { data: file } = FileAPI.useGet(id, swrConfig()) - - if (!file) { - return null - } + const { data: file, error } = FileAPI.useGet(id, swrConfig()) + const isLoading = !file && !error + const isError = !file && error + const isSuccess = file && !error return ( -
- -
+ <> + {isLoading ? : null} + {isError ? : null} + {isSuccess ? ( +
+ +
+ ) : null} + ) } diff --git a/ui/src/components/mosaic/mosaic-overview/mosaic-overview-settings.tsx b/ui/src/components/mosaic/mosaic-overview/mosaic-overview-settings.tsx index 1a03074f4..5673704af 100644 --- a/ui/src/components/mosaic/mosaic-overview/mosaic-overview-settings.tsx +++ b/ui/src/components/mosaic/mosaic-overview/mosaic-overview-settings.tsx @@ -9,7 +9,7 @@ // licenses/AGPL.txt. import { useCallback, useMemo } from 'react' import { Button, Card, CardBody, CardFooter, Text } from '@chakra-ui/react' -import { IconBolt, IconDelete } from '@koupr/ui' +import { IconBolt, IconDelete, SectionError, SectionSpinner } from '@koupr/ui' import cx from 'classnames' import FileAPI from '@/client/api/file' import MosaicAPI from '@/client/api/mosaic' @@ -32,11 +32,16 @@ const MosaicOverviewSettings = () => { ) const mutateFiles = useAppSelector((state) => state.ui.files.mutate) const mutateTaskCount = useAppSelector((state) => state.ui.tasks.mutateCount) - const { data: info, mutate: mutateInfo } = MosaicAPI.useGetInfo( - id, - swrConfig(), - ) - const { data: file, mutate: mutateFile } = FileAPI.useGet(id, swrConfig()) + const { + data: info, + error: infoError, + mutate: mutateInfo, + } = MosaicAPI.useGetInfo(id, swrConfig()) + const { + data: file, + error: fileError, + mutate: mutateFile, + } = FileAPI.useGet(id, swrConfig()) const handleUpdate = useCallback(async () => { if (id) { @@ -48,6 +53,12 @@ const MosaicOverviewSettings = () => { dispatch(modalDidClose()) } }, [id, mutateFile, mutateFiles, mutateTaskCount, mutateInfo, dispatch]) + const isFileLoading = !file && !fileError + const isFileError = !file && fileError + const isFileSuccess = file && !fileError + const isInfoLoading = !info && !infoError + const isInfoError = !info && infoError + const isInfoSuccess = info && !infoError const handleDelete = useCallback(async () => { if (id) { @@ -76,42 +87,50 @@ const MosaicOverviewSettings = () => { ) }, [info, file]) - if (!file || !info) { - return null - } - return ( -
- - - Create a mosaic for the active snapshot. - - - - - - - - Delete mosaic from the active snapshot. - - - - - -
+ <> + {isFileLoading ? : null} + {isFileError ? : null} + {isFileSuccess ? ( + <> + {isInfoLoading ? : null} + {isInfoError ? : null} + {isInfoSuccess ? ( +
+ + + Create a mosaic for the active snapshot. + + + + + + + + Delete mosaic from the active snapshot. + + + + + +
+ ) : null} + + ) : null} + ) } From f2fb1b089f0d2ef9d27e72b39b2a2fee3503984b Mon Sep 17 00:00:00 2001 From: Anass Bouassaba Date: Tue, 12 Nov 2024 19:34:28 +0100 Subject: [PATCH 03/13] wip(ui): error handling for snapshots --- ui/src/components/snapshot/snapshot-list.tsx | 161 +++++++++---------- 1 file changed, 78 insertions(+), 83 deletions(-) diff --git a/ui/src/components/snapshot/snapshot-list.tsx b/ui/src/components/snapshot/snapshot-list.tsx index e3a6e23d6..32bc73e85 100644 --- a/ui/src/components/snapshot/snapshot-list.tsx +++ b/ui/src/components/snapshot/snapshot-list.tsx @@ -68,6 +68,10 @@ const SnapshotList = () => { }, swrConfig(), ) + const isLoading = !list && !error + const isError = !list && error + const isEmpty = list && !error && list.totalElements === 0 + const isSuccess = list && !error && list.totalElements > 0 useEffect(() => { if (snapshotMutate) { @@ -129,95 +133,86 @@ const SnapshotList = () => { Snapshots - {!list && error ? ( - + {isLoading ? : null} + {isError ? : null} + {isEmpty ? ( + ) : null} - {!list && !error ? : null} - {list && !error ? ( - <> - {list.totalElements > 0 ? ( -
- - - - - - - {list.data.map((s) => ( - +
+ + + + + + {list.data.map((s) => ( + handleSelect(s)} + > + + - - - ))} - -
+ + +
handleSelect(s)} > -
- - -
- -
- - - -
- {s.original.size ? ( - - {prettyBytes(s.original.size)} - - ) : null} - {s.entities ? ( - Insights - ) : null} - {s.mosaic ? ( - Mosaic - ) : null} - {s.isActive ? ( - Active - ) : null} -
-
+ +
+ + + +
+ {s.original.size ? ( + + {prettyBytes(s.original.size)} + + ) : null} + {s.entities ? ( + Insights + ) : null} + {s.mosaic ? ( + Mosaic + ) : null} + {s.isActive ? ( + Active + ) : null}
-
- {list.totalPages > 1 ? ( -
- setPage(value)} - /> -
- ) : null} +
+ + + + ))} + + + {list.totalPages > 1 ? ( +
+ setPage(value)} + />
- ) : ( - - )} - + ) : null} + ) : null}
From 0b99c65dcc975d98f5b303cbb161bc1bcfbfa359 Mon Sep 17 00:00:00 2001 From: Anass Bouassaba Date: Tue, 12 Nov 2024 19:49:57 +0100 Subject: [PATCH 04/13] wip(ui): error handling for tasks --- ui/src/components/task/task-drawer.tsx | 43 +++++++++----- ui/src/components/task/task-list.tsx | 81 +++++++++++++------------- 2 files changed, 70 insertions(+), 54 deletions(-) diff --git a/ui/src/components/task/task-drawer.tsx b/ui/src/components/task/task-drawer.tsx index a29d7fdff..c01367eba 100644 --- a/ui/src/components/task/task-drawer.tsx +++ b/ui/src/components/task/task-drawer.tsx @@ -9,7 +9,7 @@ // licenses/AGPL.txt. import { useCallback, useEffect, useState } from 'react' import { Button, useDisclosure } from '@chakra-ui/react' -import { AuxiliaryDrawer } from '@koupr/ui' +import { AuxiliaryDrawer, SectionError, SectionSpinner } from '@koupr/ui' import { IconClearAll, IconStacks } from '@koupr/ui' import cx from 'classnames' import TaskAPI from '@/client/api/task' @@ -24,7 +24,14 @@ const TaskDrawer = () => { const [isDismissing, setIsDismissing] = useState(false) const isDrawerOpen = useAppSelector((state) => state.ui.tasks.isDrawerOpen) const mutateList = useAppSelector((state) => state.ui.tasks.mutateList) - const { data: count, mutate: mutateCount } = TaskAPI.useGetCount(swrConfig()) + const { + data: count, + error, + mutate: mutateCount, + } = TaskAPI.useGetCount(swrConfig()) + const isLoading = !count && !error + const isError = !count && error + const isSuccess = count && !error useEffect(() => { if (isDrawerOpen) { @@ -58,19 +65,29 @@ const TaskDrawer = () => { onOpen={onOpen} hasBadge={count !== undefined && count > 0} header="Tasks" - body={} + body={ + <> + {isLoading ? : null} + {isError ? : null} + {isSuccess ? : null} + + } footer={ <> - {count && count > 0 ? ( - + {isSuccess ? ( + <> + {count && count > 0 ? ( + + ) : null} + ) : null} } diff --git a/ui/src/components/task/task-list.tsx b/ui/src/components/task/task-list.tsx index fa5cc020f..25a574fcc 100644 --- a/ui/src/components/task/task-list.tsx +++ b/ui/src/components/task/task-list.tsx @@ -29,6 +29,10 @@ const TasksList = () => { error, mutate: mutateList, } = TaskAPI.useList({ page, size: 5, sortOrder: SortOrder.Asc }, swrConfig()) + const isLoading = !list && !error + const isError = !list && error + const isEmpty = list && !error && list.totalElements === 0 + const isSuccess = list && !error && list.totalElements > 0 useEffect(() => { dispatch(mutateListUpdated(mutateList)) @@ -36,48 +40,43 @@ const TasksList = () => { return ( <> - {!list && error ? : null} - {!list && !error ? : null} - {list && !error ? ( - <> - {list.totalElements > 0 ? ( -
-
- {list.data.map((task) => ( - - ))} -
- {list.totalPages > 1 ? ( - setPage(value)} - /> - ) : null} -
- ) : ( - + {isLoading ? : null} + {isError ? : null} + {isEmpty ? : null} + {isSuccess ? ( +
+ > +
+ {list.data.map((task) => ( + + ))} +
+ {list.totalPages > 1 ? ( + setPage(value)} + /> + ) : null} +
) : null} ) From c4e5e73cf3fe10654e7e3e80d4bedd2d06bf002c Mon Sep 17 00:00:00 2001 From: Anass Bouassaba Date: Tue, 12 Nov 2024 20:44:49 +0100 Subject: [PATCH 05/13] refactor(ui): variable naming --- ui/src/components/common/group-selector.tsx | 20 ++++++++++--------- .../common/organization-selector.tsx | 18 ++++++++--------- ui/src/components/common/user-selector.tsx | 20 ++++++++++--------- ui/src/components/insights/index.tsx | 8 ++++---- .../components/insights/insights-create.tsx | 8 ++++---- .../insights-overview-artifacts.tsx | 14 ++++++------- .../insights-overview-chart.tsx | 18 ++++++++--------- .../insights-overview-entities.tsx | 20 ++++++++++--------- .../insights-overview-settings.tsx | 8 ++++---- ui/src/components/mosaic/index.tsx | 8 ++++---- .../mosaic/mosaic-overview/index.tsx | 14 ++++++------- .../mosaic-overview-artifacts.tsx | 14 ++++++------- .../mosaic-overview-settings.tsx | 16 +++++++-------- ui/src/components/snapshot/snapshot-list.tsx | 20 ++++++++++--------- ui/src/components/task/task-drawer.tsx | 16 +++++++-------- ui/src/components/task/task-list.tsx | 18 ++++++++--------- 16 files changed, 124 insertions(+), 116 deletions(-) diff --git a/ui/src/components/common/group-selector.tsx b/ui/src/components/common/group-selector.tsx index 22a938e75..2d7bd4d40 100644 --- a/ui/src/components/common/group-selector.tsx +++ b/ui/src/components/common/group-selector.tsx @@ -53,16 +53,16 @@ const GroupSelector = ({ const [selected, setSelected] = useState() const { data: list, - error, + error: listError, mutate, } = GroupAPI.useList( { query, organizationId, page, size: 5, sortOrder: SortOrder.Desc }, swrConfig(), ) - const isLoading = !list && !error - const isError = !list && error - const isEmpty = list && !error && list.totalElements === 0 - const isSuccess = list && !error && list.totalElements > 0 + const isListLoading = !list && !listError + const isListError = !list && listError + const isListEmpty = list && !listError && list.totalElements === 0 + const isListReady = list && !listError && list.totalElements > 0 useEffect(() => { mutate().then() @@ -120,12 +120,14 @@ const GroupSelector = ({ query={query} onChange={handleSearchInputChange} /> - {isLoading ? : null} - {isError ? : null} - {isEmpty ? ( + {isListLoading ? : null} + {isListError ? ( + + ) : null} + {isListEmpty ? ( ) : null} - {isSuccess ? ( + {isListReady ? (
{ const [confirmed, setConfirmed] = useState() const { data: list, - error, + error: listError, mutate, } = OrganizationAPI.useList( { query, page, size: 5, sortOrder: SortOrder.Desc }, swrConfig(), ) - const isLoading = !list && !error - const isError = !list && error - const isEmpty = list && !error && list.totalElements === 0 - const isSuccess = list && !error && list.totalElements > 0 + const isListLoading = !list && !listError + const isListError = !list && listError + const isListEmpty = list && !listError && list.totalElements === 0 + const isListReady = list && !listError && list.totalElements > 0 useEffect(() => { mutate().then() @@ -119,14 +119,14 @@ const OrganizationSelector = ({ onConfirm }: OrganizationSelectorProps) => { query={query} onChange={handleSearchInputChange} /> - {isLoading ? : null} - {isError ? ( + {isListLoading ? : null} + {isListError ? ( ) : null} - {isEmpty ? ( + {isListEmpty ? ( ) : null} - {isSuccess ? ( + {isListReady ? (
() const { data: list, - error, + error: listError, mutate, } = UserAPI.useList( { @@ -73,10 +73,10 @@ const UserSelector = ({ }, swrConfig(), ) - const isLoading = !list && !error - const isError = !list && error - const isEmpty = list && !error && list.totalElements === 0 - const isSuccess = list && !error && list.totalElements > 0 + const isListLoading = !list && !listError + const isListError = !list && listError + const isListEmpty = list && !listError && list.totalElements === 0 + const isListReady = list && !listError && list.totalElements > 0 useEffect(() => { mutate().then() @@ -134,12 +134,14 @@ const UserSelector = ({ query={query} onChange={handleSearchInputChange} /> - {isError ? : null} - {isLoading ? : null} - {isEmpty ? ( + {isListError ? ( + + ) : null} + {isListLoading ? : null} + {isListEmpty ? ( ) : null} - {isSuccess ? ( + {isListReady ? (
{ const { data: file, error: fileError } = FileAPI.useGet(id, swrConfig()) const isInfoLoading = !info && !infoError const isInfoError = !info && infoError - const isInfoSuccess = info && !infoError + const isInfoReady = info && !infoError const isFileLoading = !file && !fileError const isFileError = !file && fileError - const isFileSuccess = file && !fileError + const isFileReady = file && !fileError useEffect(() => { if (mutateInfo) { @@ -70,11 +70,11 @@ const Insights = () => { {isFileLoading ? : null} {isFileError ? : null} - {isFileSuccess ? ( + {isFileReady ? ( <> {isInfoLoading ? : null} {isInfoError ? : null} - {isInfoSuccess ? ( + {isInfoReady ? ( <> {info?.isAvailable ? : } diff --git a/ui/src/components/insights/insights-create.tsx b/ui/src/components/insights/insights-create.tsx index cd1162c92..1abd4009e 100644 --- a/ui/src/components/insights/insights-create.tsx +++ b/ui/src/components/insights/insights-create.tsx @@ -51,10 +51,10 @@ const InsightsCreate = () => { }, [file, languages]) const isFileLoading = !file && !fileError const isFileError = !file && fileError - const isFileSuccess = file && !fileError + const isFileReady = file && !fileError const isLanguagesLoading = !languages && !languagesError const isLanguagesError = !languages && languagesError - const isLanguagesSuccess = languages && !languagesError + const isLanguagesReady = languages && !languagesError const handleCreate = useCallback(async () => { if (id && language) { @@ -80,13 +80,13 @@ const InsightsCreate = () => { {isFileLoading ? : null} {isFileError ? : null} - {isFileSuccess ? ( + {isFileReady ? ( <> {isLanguagesLoading ? : null} {isLanguagesError ? ( ) : null} - {isLanguagesSuccess ? ( + {isLanguagesReady ? (
{ ? state.ui.files.selection[0] : undefined, ) - const { data: file, error } = FileAPI.useGet(id, swrConfig()) + const { data: file, error: fileError } = FileAPI.useGet(id, swrConfig()) const searchParams = new URLSearchParams({ access_token: getAccessTokenOrRedirect(), }) - const isLoading = !file && !error - const isError = !file && error - const isSuccess = file && !error + const isFileLoading = !file && !fileError + const isFileError = !file && fileError + const isFileReady = file && !fileError return ( <> - {isLoading ? : null} - {isError ? : null} - {isSuccess ? ( + {isFileLoading ? : null} + {isFileError ? : null} + {isFileReady ? (
{ ? state.ui.files.selection[0] : undefined, ) - const { data: list, error } = InsightsAPI.useListEntities( + const { data: list, error: listError } = InsightsAPI.useListEntities( id, { page: 1, size: 5, sortBy: SortBy.Frequency, sortOrder: SortOrder.Desc }, swrConfig(), ) - const isLoading = !list && !error - const isError = !list && error - const isEmpty = list && !error && list.totalElements < 5 - const isSuccess = list && !error && list.totalElements >= 5 + const isListLoading = !list && !listError + const isListError = !list && listError + const isListEmpty = list && !listError && list.totalElements < 5 + const isListReady = list && !listError && list.totalElements >= 5 return ( <> - {isLoading ? : null} - {isError ? : null} - {isEmpty ? ( + {isListLoading ? : null} + {isListError ? : null} + {isListEmpty ? ( ) : null} - {isSuccess ? ( + {isListReady ? (
{ const { data: metadata } = InsightsAPI.useGetInfo(id, swrConfig()) const { data: list, - error, + error: listError, mutate, } = InsightsAPI.useListEntities( metadata ? id : undefined, @@ -45,10 +45,10 @@ const InsightsOverviewEntities = () => { }, query ? undefined : swrConfig(), ) - const isLoading = !list && !error - const isError = !list && error - const isEmpty = list && !error && list.totalElements === 0 - const isSuccess = list && !error && list.totalElements > 0 + const isListLoading = !list && !listError + const isListError = !list && listError + const isListEmpty = list && !listError && list.totalElements === 0 + const isListReady = list && !listError && list.totalElements > 0 useEffect(() => { mutate().then() @@ -72,10 +72,12 @@ const InsightsOverviewEntities = () => { onValue={handleSearchInputValue} onClear={handleSearchInputClear} /> - {isLoading ? : null} - {isError ? : null} - {isEmpty ? : null} - {isSuccess ? ( + {isListLoading ? : null} + {isListError ? : null} + {isListEmpty ? ( + + ) : null} + {isListReady ? (
{ }, [info, file]) const isFileLoading = !file && !fileError const isFileError = !file && fileError - const isFileSuccess = file && !fileError + const isFileReady = file && !fileError const isInfoLoading = !info && !infoError const isInfoError = !info && infoError - const isInfoSuccess = info && !infoError + const isInfoReady = info && !infoError const handleUpdate = useCallback(async () => { if (id) { @@ -90,11 +90,11 @@ const InsightsOverviewSettings = () => { <> {isFileLoading ? : null} {isFileError ? : null} - {isFileSuccess ? ( + {isFileReady ? ( <> {isInfoLoading ? : null} {isInfoError ? : null} - {isInfoSuccess ? ( + {isInfoReady ? (
diff --git a/ui/src/components/mosaic/index.tsx b/ui/src/components/mosaic/index.tsx index 5793eb664..363761b10 100644 --- a/ui/src/components/mosaic/index.tsx +++ b/ui/src/components/mosaic/index.tsx @@ -41,10 +41,10 @@ const Mosaic = () => { const { data: file, error: fileError } = FileAPI.useGet(id, swrConfig()) const isFileLoading = !file && !fileError const isFileError = !file && fileError - const isFileSuccess = file && !fileError + const isFileReady = file && !fileError const isInfoLoading = !info && !infoError const isInfoError = !info && infoError - const isInfoSuccess = info && !infoError + const isInfoReady = info && !infoError useEffect(() => { if (file?.snapshot?.task?.isPending) { @@ -71,11 +71,11 @@ const Mosaic = () => { {isFileLoading ? : null} {isFileError ? : null} - {isFileSuccess ? ( + {isFileReady ? ( <> {isInfoLoading ? : null} {isInfoError ? : null} - {isInfoSuccess ? ( + {isInfoReady ? ( <>{info.isAvailable ? : } ) : null} diff --git a/ui/src/components/mosaic/mosaic-overview/index.tsx b/ui/src/components/mosaic/mosaic-overview/index.tsx index 59d090dd1..2a1eeaf83 100644 --- a/ui/src/components/mosaic/mosaic-overview/index.tsx +++ b/ui/src/components/mosaic/mosaic-overview/index.tsx @@ -36,17 +36,17 @@ const MosaicOverview = () => { : undefined, ) const [isWarningVisible, setIsWarningVisible] = useState(true) - const { data: info, error } = MosaicAPI.useGetInfo(id, swrConfig()) - const isLoading = !info && !error - const isError = !info && error - const isSuccess = info && !error + const { data: info, error: infoError } = MosaicAPI.useGetInfo(id, swrConfig()) + const isInfoLoading = !info && !infoError + const isInfoError = !info && infoError + const isInfoReady = info && !infoError return ( <> - {isLoading ? : null} - {isError ? : null} - {isSuccess ? ( + {isInfoLoading ? : null} + {isInfoError ? : null} + {isInfoReady ? (
{info.isOutdated && isWarningVisible ? ( diff --git a/ui/src/components/mosaic/mosaic-overview/mosaic-overview-artifacts.tsx b/ui/src/components/mosaic/mosaic-overview/mosaic-overview-artifacts.tsx index 66646150c..e6b7e910e 100644 --- a/ui/src/components/mosaic/mosaic-overview/mosaic-overview-artifacts.tsx +++ b/ui/src/components/mosaic/mosaic-overview/mosaic-overview-artifacts.tsx @@ -20,16 +20,16 @@ const MosaicOverviewArtifacts = () => { ? state.ui.files.selection[0] : undefined, ) - const { data: file, error } = FileAPI.useGet(id, swrConfig()) - const isLoading = !file && !error - const isError = !file && error - const isSuccess = file && !error + const { data: file, error: fileError } = FileAPI.useGet(id, swrConfig()) + const isFileLoading = !file && !fileError + const isFileError = !file && fileError + const isFileReady = file && !fileError return ( <> - {isLoading ? : null} - {isError ? : null} - {isSuccess ? ( + {isFileLoading ? : null} + {isFileError ? : null} + {isFileReady ? (
{ error: fileError, mutate: mutateFile, } = FileAPI.useGet(id, swrConfig()) + const isFileLoading = !file && !fileError + const isFileError = !file && fileError + const isFileReady = file && !fileError + const isInfoLoading = !info && !infoError + const isInfoError = !info && infoError + const isInfoReady = info && !infoError const handleUpdate = useCallback(async () => { if (id) { @@ -53,12 +59,6 @@ const MosaicOverviewSettings = () => { dispatch(modalDidClose()) } }, [id, mutateFile, mutateFiles, mutateTaskCount, mutateInfo, dispatch]) - const isFileLoading = !file && !fileError - const isFileError = !file && fileError - const isFileSuccess = file && !fileError - const isInfoLoading = !info && !infoError - const isInfoError = !info && infoError - const isInfoSuccess = info && !infoError const handleDelete = useCallback(async () => { if (id) { @@ -91,11 +91,11 @@ const MosaicOverviewSettings = () => { <> {isFileLoading ? : null} {isFileError ? : null} - {isFileSuccess ? ( + {isFileReady ? ( <> {isInfoLoading ? : null} {isInfoError ? : null} - {isInfoSuccess ? ( + {isInfoReady ? (
diff --git a/ui/src/components/snapshot/snapshot-list.tsx b/ui/src/components/snapshot/snapshot-list.tsx index 32bc73e85..3ef064d04 100644 --- a/ui/src/components/snapshot/snapshot-list.tsx +++ b/ui/src/components/snapshot/snapshot-list.tsx @@ -56,7 +56,7 @@ const SnapshotList = () => { const [selected, setSelected] = useState() const { data: list, - error, + error: listError, mutate: snapshotMutate, } = SnapshotAPI.useList( { @@ -68,10 +68,10 @@ const SnapshotList = () => { }, swrConfig(), ) - const isLoading = !list && !error - const isError = !list && error - const isEmpty = list && !error && list.totalElements === 0 - const isSuccess = list && !error && list.totalElements > 0 + const isListLoading = !list && !listError + const isListError = !list && listError + const isListEmpty = list && !listError && list.totalElements === 0 + const isListReady = list && !listError && list.totalElements > 0 useEffect(() => { if (snapshotMutate) { @@ -133,12 +133,14 @@ const SnapshotList = () => { Snapshots - {isLoading ? : null} - {isError ? : null} - {isEmpty ? ( + {isListLoading ? : null} + {isListError ? ( + + ) : null} + {isListEmpty ? ( ) : null} - {isSuccess ? ( + {isListReady ? (
diff --git a/ui/src/components/task/task-drawer.tsx b/ui/src/components/task/task-drawer.tsx index c01367eba..6ad48c131 100644 --- a/ui/src/components/task/task-drawer.tsx +++ b/ui/src/components/task/task-drawer.tsx @@ -26,12 +26,12 @@ const TaskDrawer = () => { const mutateList = useAppSelector((state) => state.ui.tasks.mutateList) const { data: count, - error, + error: countError, mutate: mutateCount, } = TaskAPI.useGetCount(swrConfig()) - const isLoading = !count && !error - const isError = !count && error - const isSuccess = count && !error + const isCountLoading = !count && !countError + const isCountError = !count && countError + const isCountReady = count && !countError useEffect(() => { if (isDrawerOpen) { @@ -67,14 +67,14 @@ const TaskDrawer = () => { header="Tasks" body={ <> - {isLoading ? : null} - {isError ? : null} - {isSuccess ? : null} + {isCountLoading ? : null} + {isCountError ? : null} + {isCountReady ? : null} } footer={ <> - {isSuccess ? ( + {isCountReady ? ( <> {count && count > 0 ? ( - - -
- - - navigate('/account/settings')}>Settings - navigate('/account/invitation')}> -
- Invitations - {invitationCount && invitationCount > 0 ? ( - - {invitationCount} - - ) : null} +
+
+ + } + variant="solid-gray" + right="5px" + bottom="10px" + position="absolute" + zIndex={1000} + title="Edit picture" + aria-label="Edit picture" + onClick={() => setIsImageModalOpen(true)} + />
- - - - -
- setIsImageModalOpen(false)} - /> -
+ + {truncateEnd(user.fullName, 50)} + +
+
+ +
+ +
+ + + navigate('/account/settings')}> + Settings + + navigate('/account/invitation')}> +
+ Invitations + {invitationCount && invitationCount > 0 ? ( + + {invitationCount} + + ) : null} +
+
+
+
+ +
+ + setIsImageModalOpen(false)} + /> + + ) : null} ) } diff --git a/ui/src/pages/account/account-settings-page.tsx b/ui/src/pages/account/account-settings-page.tsx index 89e8edff2..ad3e5e07b 100644 --- a/ui/src/pages/account/account-settings-page.tsx +++ b/ui/src/pages/account/account-settings-page.tsx @@ -46,160 +46,174 @@ const EditButton = (props: IconButtonProps) => ( const AccountSettingsPage = () => { const { colorMode, toggleColorMode } = useColorMode() - const { data: user, error } = UserAPI.useGet() + const { data: user, error: userError } = UserAPI.useGet() const { data: storageUsage, error: storageUsageError } = StorageAPI.useGetAccountUsage(swrConfig()) const [isFullNameModalOpen, setIsFullNameModalOpen] = useState(false) const [isEmailModalOpen, setIsEmailModalOpen] = useState(false) const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false) const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false) + const isUserLoading = !user && !userError + const isUserError = !user && userError + const isUserReady = user && !userError + const isStorageUsageLoading = !storageUsage && !storageUsageError + const isStorageUsageError = !storageUsage && storageUsageError + const isStorageUsageReady = storageUsage && !storageUsageError return ( <> - {!user && error ? : null} - {!user && !error ? : null} - {user && !error ? ( + {isUserLoading ? : null} + {isUserError ? : null} + {isUserReady ? ( <> - - {storageUsageError ? ( - Failed to load storage usage. - ) : null} - {storageUsage && !storageUsageError ? ( - <> - - {prettyBytes(storageUsage.bytes)} of{' '} - {prettyBytes(storageUsage.maxBytes)} used - - - - ) : null} - {!(storageUsage && !storageUsageError) ? ( - <> - Calculating… - - - ) : null} - - ), - }, - { - title: 'Basics', - rows: [ - { - label: 'Full name', - content: ( - <> - {truncateEnd(user.fullName, 50)} - setIsFullNameModalOpen(true)} - /> - - ), - }, - ], - }, - { - title: 'Credentials', - rows: [ - { - label: 'Email', - content: ( - <> - {user.pendingEmail ? ( -
- -
- -
-
- {truncateMiddle(user.pendingEmail, 50)} -
- ) : null} - {!user.pendingEmail ? ( + {isStorageUsageLoading ? : null} + {isStorageUsageError ? ( + + ) : null} + {isStorageUsageReady ? ( + + {storageUsageError ? ( + Failed to load storage usage. + ) : null} + {storageUsage && !storageUsageError ? ( + <> - {truncateMiddle( - user.pendingEmail || user.email, - 50, - )} + {prettyBytes(storageUsage.bytes)} of{' '} + {prettyBytes(storageUsage.maxBytes)} used - ) : null} + + + ) : null} + {!(storageUsage && !storageUsageError) ? ( + <> + Calculating… + + + ) : null} + + ), + }, + { + title: 'Basics', + rows: [ + { + label: 'Full name', + content: ( + <> + {truncateEnd(user.fullName, 50)} + setIsFullNameModalOpen(true)} + /> + + ), + }, + ], + }, + { + title: 'Credentials', + rows: [ + { + label: 'Email', + content: ( + <> + {user.pendingEmail ? ( +
+ +
+ +
+
+ + {truncateMiddle(user.pendingEmail, 50)} + +
+ ) : null} + {!user.pendingEmail ? ( + + {truncateMiddle( + user.pendingEmail || user.email, + 50, + )} + + ) : null} + setIsEmailModalOpen(true)} + /> + + ), + }, + { + label: 'Password', + content: ( setIsEmailModalOpen(true)} + title="Change password" + aria-label="Change password" + onClick={() => setIsPasswordModalOpen(true)} /> - - ), - }, - { - label: 'Password', - content: ( - setIsPasswordModalOpen(true)} - /> - ), - }, - ], - }, - { - title: 'Theme', - rows: [ - { - label: 'Dark mode', - content: ( - toggleColorMode()} - /> - ), - }, - ], - }, - { - title: 'Advanced', - rows: [ - { - label: 'Delete account', - content: ( - } - variant="solid" - colorScheme="red" - title="Delete account" - aria-label="Delete account" - onClick={() => setIsDeleteModalOpen(true)} - /> - ), - }, - ], - }, - ]} - /> + ), + }, + ], + }, + { + title: 'Theme', + rows: [ + { + label: 'Dark mode', + content: ( + toggleColorMode()} + /> + ), + }, + ], + }, + { + title: 'Advanced', + rows: [ + { + label: 'Delete account', + content: ( + } + variant="solid" + colorScheme="red" + title="Delete account" + aria-label="Delete account" + onClick={() => setIsDeleteModalOpen(true)} + /> + ), + }, + ], + }, + ]} + /> + ) : null} Date: Wed, 13 Nov 2024 07:10:51 +0100 Subject: [PATCH 07/13] wip(ui): error handling for multiple areas --- ui/bun.lockb | Bin 242814 -> 242815 bytes ui/package.json | 2 +- ui/src/pages/console/console-panel-groups.tsx | 162 +++---- .../console/console-panel-invitations.tsx | 198 ++++---- .../console/console-panel-organization.tsx | 301 ++++++++----- .../console/console-panel-organizations.tsx | 160 ++++--- ui/src/pages/console/console-panel-user.tsx | 333 ++++++++------ ui/src/pages/console/console-panel-users.tsx | 425 +++++++++--------- .../console/console-panel-workspaces.tsx | 181 ++++---- ui/src/pages/group/group-layout.tsx | 56 ++- ui/src/pages/group/group-list-page.tsx | 169 ++++--- ui/src/pages/group/group-members-page.tsx | 185 ++++---- ui/src/pages/group/group-settings-page.tsx | 11 +- 13 files changed, 1148 insertions(+), 1035 deletions(-) diff --git a/ui/bun.lockb b/ui/bun.lockb index 4db487768b4c6a7e1024ef68eb2eeaaeb22872f4..af7dbc38b1e693f954af199f06f4bc1568525d05 100755 GIT binary patch delta 15390 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 Ol>!Ps3#_zl=Klj`FwH;! delta 15400 zcma)Dd0kg9Hc`F+mJy)X3Je!oBeocYY0IdkT0GxudYy;c3mt?KUu zH`F5Ee$=|;u{`h6(nsri%vyb8R_B`yOx+&+((<*w`%90Q@8VtnHXg$z(LFHUL1{eWA8z6k0A`V#1`y(FoDG)~I8fCdfh1Z@nefi?oo z0QCp$uTv{1@tHwsp~pSN_>Z83KLsUU?gp&}I<=!D)c{QgC0~x!4tx5_FKXX<`e()s z6n=g%Ky3CFDEV{T>yp$E)OUzj%LGcs_yhe_L1TsrFF9QCafxxG99@BFtuRpXN;6R6 zyAOVfqe*dziB4CBiUaHLC85AoJ?5{vWu){e zmZ=O}6aC~e=a}S_bSPg@$D^Ieqk<5ak5j~WnO0ocH?s_pO5DFXg}O7-gp|>VaTyto zQPMcaqzR;cFq}kuH^E0*Ib7p2oQW>U;Yu6tNS`DHxWqh%D>dDLRnLQu%=jVtX`6BJ zDd}W~6X+-Hi(oSJXJ#Eh2MK;7!5Nk8kdm}1a0CI;u6PU z{3`m%90_r*ASgTL0x-$h{D$EF6O_ymHMPG}e_VNvY$LC&j0X za%_eo6mblc7$$*IS|x�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 Z_{uj53hyh|+A!IxLW6=Yf-2 { const [groupId, setGroupId] = useState() const { data: list, - error, + error: listError, mutate, } = ConsoleAPI.useListOrSearchObject( 'group', { page, size, query }, swrConfig(), ) + const isListLoading = !list && !listError + const isListError = !list && listError + const isListEmpty = list && !listError && list.totalElements === 0 + const isListReady = list && !listError && list.totalElements > 0 const renameRequest = useCallback( async (name: string) => { @@ -74,84 +79,85 @@ const ConsolePanelGroups = () => {
Groups - {!list && error ? : null} - {!list && !error ? : null} - {list && list.totalElements > 0 ? ( - <> - ( -
- - {group.name} -
- ), - }, - { - title: 'Organization', - renderCell: (group) => ( - - {group.organization.name} - - ), - }, - { - title: 'Created', - renderCell: (group) => ( - - ), - }, - { - title: 'Updated', - renderCell: (group) => ( - - ), - }, - ]} - actions={[ - { - label: 'Edit Name', - icon: , - onClick: async (group) => { - setCurrentName(group.name) - setGroupId(group.id) - setIsConfirmRenameOpen(true) - }, + {isListLoading ? : null} + {isListError ? : null} + {isListEmpty ? ( + + ) : null} + {isListReady ? ( + ( +
+ + {group.name} +
+ ), + }, + { + title: 'Organization', + renderCell: (group) => ( + + {group.organization.name} + + ), + }, + { + title: 'Created', + renderCell: (group) => ( + + ), + }, + { + title: 'Updated', + renderCell: (group) => ( + + ), + }, + ]} + actions={[ + { + label: 'Edit Name', + icon: , + onClick: async (group) => { + setCurrentName(group.name) + setGroupId(group.id) + setIsConfirmRenameOpen(true) }, - ]} - pagination={ - list.totalPages > 1 ? ( - - ) : undefined - } - /> - + }, + ]} + pagination={ + list.totalPages > 1 ? ( + + ) : undefined + } + /> ) : null}
{ }) const { data: list, - error, + error: listError, mutate, } = ConsoleAPI.useListObject( 'invitation', { page, size }, swrConfig(), ) + const isListLoading = !list && !listError + const isListError = !list && listError + const isListEmpty = list && !listError && list.totalElements === 0 + const isListReady = list && !listError && list.totalElements > 0 return ( <> @@ -62,109 +66,101 @@ const ConsolePanelInvitations = () => {
Invitations - {!list && error ? ( + {isListLoading ? : null} + {isListError ? ( ) : null} - {!list && !error ? : null} - {list && !error ? ( - <> - {list.totalElements > 0 ? ( - <> - ( - - ), - }, - { - title: 'Invitee', - renderCell: (invitation) => ( - {invitation.email} - ), - }, - { - title: 'Status', - renderCell: (invitation) => ( - <> - {invitation.status === 'pending' ? ( - Pending - ) : invitation.status === 'declined' ? ( - Declined - ) : invitation.status === 'accepted' ? ( - Accepted - ) : ( - Unknown - )} - - ), - }, - { - title: 'Created', - renderCell: (invitation) => ( - - ), - }, - { - title: 'Updated', - renderCell: (invitation) => ( - - ), - }, - ]} - actions={[ - { - label: 'Deny', - icon: , - isDestructive: true, - isDisabledFn: (invitation) => - invitation.status !== 'pending', - onClick: async (invitation) => { - setConfirmationHeader(<>Deny Invitation) - setConfirmationBody( - <>Are you sure you want to deny this invitation?, - ) - setConfirmationRequest(() => async () => { - await ConsoleAPI.invitationChangeStatus({ - id: invitation.id, - accept: false, - }) - await mutate() - }) - setIsConfirmationDestructive(true) - setIsConfirmationOpen(true) - }, - }, - ]} - pagination={ - list.totalPages > 1 ? ( - - ) : undefined - } + {isListEmpty ? ( + + ) : null} + {isListReady ? ( + ( + + ), + }, + { + title: 'Invitee', + renderCell: (invitation) => {invitation.email}, + }, + { + title: 'Status', + renderCell: (invitation) => ( + <> + {invitation.status === 'pending' ? ( + Pending + ) : invitation.status === 'declined' ? ( + Declined + ) : invitation.status === 'accepted' ? ( + Accepted + ) : ( + Unknown + )} + + ), + }, + { + title: 'Created', + renderCell: (invitation) => ( + + ), + }, + { + title: 'Updated', + renderCell: (invitation) => ( + + ), + }, + ]} + actions={[ + { + label: 'Deny', + icon: , + isDestructive: true, + isDisabledFn: (invitation) => invitation.status !== 'pending', + onClick: async (invitation) => { + setConfirmationHeader(<>Deny Invitation) + setConfirmationBody( + <>Are you sure you want to deny this invitation?, + ) + setConfirmationRequest(() => async () => { + await ConsoleAPI.invitationChangeStatus({ + id: invitation.id, + accept: false, + }) + await mutate() + }) + setIsConfirmationDestructive(true) + setIsConfirmationOpen(true) + }, + }, + ]} + pagination={ + list.totalPages > 1 ? ( + - - ) : ( - - )} - + ) : undefined + } + /> ) : null}
{confirmationHeader && confirmationBody && confirmationRequest ? ( diff --git a/ui/src/pages/console/console-panel-organization.tsx b/ui/src/pages/console/console-panel-organization.tsx index a5e9c2c68..532407f73 100644 --- a/ui/src/pages/console/console-panel-organization.tsx +++ b/ui/src/pages/console/console-panel-organization.tsx @@ -71,14 +71,31 @@ const ConsolePanelOrganization = () => { }, swrConfig(), ) + const isOrgLoading = !org && !orgError + const isOrgError = !org && orgError + const isOrgReady = org && !orgError + const isUserListLoading = !userList && !userError + const isUserListError = !userList && userError + const isUserListEmpty = userList && !userError && userList.totalElements === 0 + const isUserListReady = userList && !userError && userList.totalElements > 0 + const isWorkspaceListLoading = !workspaceList && !workspaceError + const isWorkspaceListError = !workspaceList && workspaceError + const isWorkspaceListEmpty = + workspaceList && !workspaceError && workspaceList.totalElements === 0 + const isWorkspaceListReady = + workspaceList && !workspaceError && workspaceList.totalElements > 0 + const isGroupListLoading = !groupList && !groupError + const isGroupListError = !groupList && groupError + const isGroupListEmpty = + groupList && !groupError && groupList.totalElements === 0 + const isGroupListReady = + groupList && !groupError && groupList.totalElements > 0 return ( <> - {!org && orgError ? ( - - ) : null} - {!org && !orgError ? : null} - {org && !orgError ? ( + {isOrgLoading ? : null} + {isOrgError ? : null} + {isOrgReady ? ( <> {org.name} @@ -105,11 +122,22 @@ const ConsolePanelOrganization = () => { - {!userList && userError ? ( - + {isUserListLoading ? ( + + + + ) : null} + {isUserListError ? ( + + + ) : null} - {!userList && !userError ? : null} - {userList && !userError ? ( + {isUserListEmpty ? ( + + + + ) : null} + {isUserListReady ? ( <>
@@ -149,56 +177,55 @@ const ConsolePanelOrganization = () => {
- {userList.totalElements > 0 ? ( -
- {userList.data.map((user) => ( - - -
-
- - {user.username} - - - {user.permission} - -
- - - +
+ {userList.data.map((user) => ( + + +
+
+ + {user.username} + + {user.permission}
- - ))} -
- ) : ( - - )} + + + +
+ + ))} +
) : null} - {!workspaceList && workspaceError ? ( - + {isWorkspaceListLoading ? ( + + + + ) : null} + {isWorkspaceListError ? ( + + + ) : null} - {!workspaceList && !workspaceError ? : null} - {workspaceList && !workspaceError ? ( + {isWorkspaceListEmpty ? ( + + + + ) : null} + {isWorkspaceListReady ? ( <> @@ -240,48 +267,55 @@ const ConsolePanelOrganization = () => {
- {workspaceList.totalElements > 0 ? ( -
- {workspaceList.data.map((workspace) => ( -
- -
- - {workspace.name} - - - - -
+
+ {workspaceList.data.map((workspace) => ( +
+ +
+ + {workspace.name} + + + +
- ))} -
- ) : ( - - )} +
+ ))} +
) : null} - {!groupList && groupError ? ( - + {isGroupListLoading ? ( + + + ) : null} - {!groupList && !groupError ? : null} - {groupList && !groupError ? ( + {isGroupListError ? ( + + + + ) : null} + {isGroupListEmpty ? ( + + + + ) : null} + {isGroupListReady ? ( <> @@ -321,39 +355,33 @@ const ConsolePanelOrganization = () => {
- {groupList.totalElements > 0 ? ( -
- {groupList.data.map((group) => ( -
- -
- - {group.name} - - - - -
+
+ {groupList.data.map((group) => ( +
+ +
+ + {group.name} + + + +
- ))} -
- ) : ( - - )} +
+ ))} +
) : null} @@ -365,4 +393,35 @@ const ConsolePanelOrganization = () => { ) } +type ListSkeletonProps = { + header: string + children?: React.ReactNode +} + +const ListSkeleton = ({ header, children }: ListSkeletonProps) => ( + <> + + + + + + +
+
+ {header} + +
+
+ + {children} + +) + export default ConsolePanelOrganization diff --git a/ui/src/pages/console/console-panel-organizations.tsx b/ui/src/pages/console/console-panel-organizations.tsx index d32eab188..3f15908d4 100644 --- a/ui/src/pages/console/console-panel-organizations.tsx +++ b/ui/src/pages/console/console-panel-organizations.tsx @@ -49,13 +49,18 @@ const ConsolePanelOrganizations = () => { const [organizationId, setOrganizationId] = useState() const { data: list, - error, + error: listError, mutate, } = ConsoleAPI.useListOrSearchObject( 'organization', { page, size, query }, swrConfig(), ) + const isListLoading = !list && !listError + const isListError = !list && listError + const isListEmpty = list && !listError && list.totalElements === 0 + const isListReady = list && !listError && list.totalElements > 0 + const renameRequest = useCallback( async (name: string) => { if (organizationId) { @@ -76,91 +81,82 @@ const ConsolePanelOrganizations = () => {
Organizations - {!list && error ? ( + {isListLoading ? : null} + {isListError ? ( ) : null} - {!list && !error ? : null} - {list && !error ? ( - <> - {list.totalElements > 0 ? ( - <> - ( -
- + {isListEmpty ? ( + + ) : null} + {isListReady ? ( + ( +
+ - - {organization.name} - -
- ), - }, - { - title: 'Created', - renderCell: (organization) => ( - - ), - }, - { - title: 'Updated', - renderCell: (organization) => ( - - ), - }, - ]} - actions={[ - { - label: 'Edit Name', - icon: , - onClick: async (organization) => { - setCurrentName(organization.name) - setOrganizationId(organization.id) - setIsConfirmRenameOpen(true) - }, - }, - ]} - pagination={ - list.totalPages > 1 ? ( - - ) : undefined - } + + {organization.name} + +
+ ), + }, + { + title: 'Created', + renderCell: (organization) => ( + + ), + }, + { + title: 'Updated', + renderCell: (organization) => ( + + ), + }, + ]} + actions={[ + { + label: 'Edit Name', + icon: , + onClick: async (organization) => { + setCurrentName(organization.name) + setOrganizationId(organization.id) + setIsConfirmRenameOpen(true) + }, + }, + ]} + pagination={ + list.totalPages > 1 ? ( + - - ) : ( - - )} - + ) : undefined + } + /> ) : null}
{ }, swrConfig(), ) + const isUserLoading = !user && !userError + const isUserError = !user && userError + const isUserReady = user && !userError + const isGroupListLoading = !groupList && !groupError + const isGroupListError = !groupList && groupError + const isGroupListEmpty = + groupList && !groupError && groupList.totalElements === 0 + const isGroupListReady = + groupList && !groupError && groupList.totalElements > 0 + const isOrgListLoading = !orgList && !orgError + const isOrgListError = !orgList && orgError + const isOrgListEmpty = orgList && !orgError && orgList.totalElements === 0 + const isOrgListReady = orgList && !orgError && orgList.totalElements > 0 + const isWorkspaceListLoading = !workspaceList && !workspaceError + const isWorkspaceListError = !workspaceList && workspaceError + const isWorkspaceListEmpty = + workspaceList && !workspaceError && workspaceList.totalElements === 0 + const isWorkspaceListReady = + workspaceList && !workspaceError && workspaceList.totalElements > 0 return ( <> - {!user && userError ? : null} - {!user && !userError ? : null} - {user && !userError ? ( + {isUserLoading ? : null} + {isUserError ? : null} + {isUserReady ? ( <> {user.fullName} @@ -226,11 +245,22 @@ const ConsolePanelUser = () => { /> - {!orgList && orgError ? ( - + {isOrgListLoading ? ( + + + ) : null} - {!orgList && !orgError ? : null} - {orgList && !orgError ? ( + {isOrgListEmpty ? ( + + + + ) : null} + {isOrgListError ? ( + + + + ) : null} + {isOrgListReady ? ( <> @@ -272,55 +302,62 @@ const ConsolePanelUser = () => {
- {orgList.totalElements > 0 ? ( -
- {orgList.data.map((organization) => ( -
- -
-
- - {organization.organizationName} - - - {organization.permission} - -
- - - +
+ {orgList.data.map((organization) => ( +
+ +
+
+ + {organization.organizationName} + + + {organization.permission} +
+ + +
- ))} -
- ) : ( - - )} +
+ ))} +
) : null} - {!workspaceList && workspaceError ? ( - + {isWorkspaceListLoading ? ( + + + ) : null} - {!workspaceList && !workspaceError ? : null} - {workspaceList && !workspaceError ? ( + {isWorkspaceListError ? ( + + + + ) : null} + {isWorkspaceListEmpty ? ( + + + + ) : null} + {isWorkspaceListReady ? ( <> @@ -360,55 +397,62 @@ const ConsolePanelUser = () => {
- {workspaceList.totalElements > 0 ? ( -
- {workspaceList.data.map((workspace) => ( -
- -
-
- - {workspace.workspaceName} - - - {workspace.permission} - -
- - - +
+ {workspaceList.data.map((workspace) => ( +
+ +
+
+ + {workspace.workspaceName} + + + {workspace.permission} +
+ + +
- ))} -
- ) : ( - - )} +
+ ))} +
) : null} - {!groupList && groupError ? ( - + {isGroupListLoading ? ( + + + + ) : null} + {isGroupListError ? ( + + + ) : null} - {!groupList && !groupError ? : null} - {groupList && !groupError ? ( + {isGroupListEmpty ? ( + + + + ) : null} + {isGroupListReady ? ( <> @@ -448,49 +492,43 @@ const ConsolePanelUser = () => {
- {groupList.totalElements > 0 ? ( -
- {groupList.data.map((group) => ( -
- -
-
- - {group.groupName} - - - {group.permission} - -
- - - +
+ {groupList.data.map((group) => ( +
+ +
+
+ + {group.groupName} + + {group.permission}
+ + +
- ))} -
- ) : ( - - )} +
+ ))} +
) : null} @@ -501,4 +539,35 @@ const ConsolePanelUser = () => { ) } +type ListSekeletonProps = { + header: string + children?: React.ReactNode +} + +const ListSekeleton = ({ header, children }: ListSekeletonProps) => ( + <> + + + + + + +
+
+ {header} + +
+
+ + {children} + +) + export default ConsolePanelUser diff --git a/ui/src/pages/console/console-panel-users.tsx b/ui/src/pages/console/console-panel-users.tsx index 881179c0e..8778fff66 100644 --- a/ui/src/pages/console/console-panel-users.tsx +++ b/ui/src/pages/console/console-panel-users.tsx @@ -61,9 +61,13 @@ const ConsolePanelUsers = () => { }) const { data: list, - error, + error: listError, mutate, } = UserAPI.useList({ query, page, size }, swrConfig()) + const isListLoading = !list && !listError + const isListError = !list && listError + const isListEmpty = list && !listError && list.totalElements === 0 + const isListReady = list && !listError && list.totalElements > 0 return ( <> @@ -72,223 +76,210 @@ const ConsolePanelUsers = () => {
Users - {!list && error ? : null} - {!list && !error ? : null} - {list && !error ? ( - <> - {list.totalElements > 0 ? ( - <> - ( -
- - - {user.fullName} - -
- ), - }, - { - title: 'Email', - renderCell: (user) => ( - {user.email} - ), - }, - { - title: 'Email Confirmed', - renderCell: (user) => ( - - {user.isEmailConfirmed ? 'Confirmed' : 'Awaiting'} - - ), - }, - { - title: 'Created', - renderCell: (user) => ( - - ), - }, - { - title: 'Updated', - renderCell: (user) => ( - - ), - }, - { - title: 'Props', - renderCell: (user) => ( -
- {user.isAdmin ? ( - - Admin - - ) : null} - {user.isActive ? ( - - Active - - ) : ( - - Inactive - - )} - {getUserId() === user.id ? ( - It's you - ) : null} -
- ), - }, - ]} - actions={[ - { - label: 'Suspend', - icon: , - isDestructive: true, - isHiddenFn: (user) => - getUserId() === user.id || - user.isAdmin || - !user.isActive, - onClick: async (user) => { - setConfirmationHeader(<>Suspend User) - setConfirmationBody( - <> - Are you sure you want to suspend{' '} - - {userToString(user)} - - ? - , - ) - setConfirmationRequest(() => async () => { - await UserAPI.suspend(user.id, { suspend: true }) - await mutate() - }) - setIsConfirmationDestructive(true) - setIsConfirmationOpen(true) - }, - }, - { - label: 'Unsuspend', - icon: , - isHiddenFn: (user) => user.isActive, - onClick: async (user) => { - setConfirmationHeader(<>Unsuspend User) - setConfirmationBody( - <> - Are you sure you want to unsuspend{' '} - - {userToString(user)} - - ? - , - ) - setConfirmationRequest(() => async () => { - await UserAPI.suspend(user.id, { suspend: false }) - await mutate() - }) - setIsConfirmationDestructive(false) - setIsConfirmationOpen(true) - }, - }, - { - label: 'Make Admin', - icon: , - isHiddenFn: (user) => user.isAdmin, - onClick: async (user) => { - setConfirmationHeader(<>Make Admin) - setConfirmationBody( - <> - Are you sure you want to make{' '} - - {userToString(user)} - {' '} - admin? - , - ) - setConfirmationRequest(() => async () => { - await UserAPI.makeAdmin(user.id, { makeAdmin: true }) - await mutate() - }) - setIsConfirmationDestructive(false) - setIsConfirmationOpen(true) - }, - }, - { - label: 'Demote Admin', - icon: , - isDestructive: true, - isHiddenFn: (user) => !user.isAdmin, - onClick: async (user) => { - setConfirmationHeader(<>Demote Admin) - setConfirmationBody( - <> - Are you sure you want to demote{' '} - - {userToString(user)} - - ? - , - ) - setConfirmationRequest(() => async () => { - await UserAPI.makeAdmin(user.id, { makeAdmin: false }) - await mutate() - if (getUserId() === user.id) { - navigate('/sign-out') - } - }) - setIsConfirmationDestructive(true) - setIsConfirmationOpen(true) - }, - }, - ]} - pagination={ - list.totalPages > 1 ? ( - - ) : undefined - } + {isListLoading ? : null} + {isListError ? : null} + {isListEmpty ? : null} + {isListReady ? ( + ( +
+ + + {user.fullName} + +
+ ), + }, + { + title: 'Email', + renderCell: (user) => {user.email}, + }, + { + title: 'Email Confirmed', + renderCell: (user) => ( + + {user.isEmailConfirmed ? 'Confirmed' : 'Awaiting'} + + ), + }, + { + title: 'Created', + renderCell: (user) => ( + + ), + }, + { + title: 'Updated', + renderCell: (user) => ( + + ), + }, + { + title: 'Props', + renderCell: (user) => ( +
+ {user.isAdmin ? ( + + Admin + + ) : null} + {user.isActive ? ( + + Active + + ) : ( + + Inactive + + )} + {getUserId() === user.id ? ( + It's you + ) : null} +
+ ), + }, + ]} + actions={[ + { + label: 'Suspend', + icon: , + isDestructive: true, + isHiddenFn: (user) => + getUserId() === user.id || user.isAdmin || !user.isActive, + onClick: async (user) => { + setConfirmationHeader(<>Suspend User) + setConfirmationBody( + <> + Are you sure you want to suspend{' '} + + {userToString(user)} + + ? + , + ) + setConfirmationRequest(() => async () => { + await UserAPI.suspend(user.id, { suspend: true }) + await mutate() + }) + setIsConfirmationDestructive(true) + setIsConfirmationOpen(true) + }, + }, + { + label: 'Unsuspend', + icon: , + isHiddenFn: (user) => user.isActive, + onClick: async (user) => { + setConfirmationHeader(<>Unsuspend User) + setConfirmationBody( + <> + Are you sure you want to unsuspend{' '} + + {userToString(user)} + + ? + , + ) + setConfirmationRequest(() => async () => { + await UserAPI.suspend(user.id, { suspend: false }) + await mutate() + }) + setIsConfirmationDestructive(false) + setIsConfirmationOpen(true) + }, + }, + { + label: 'Make Admin', + icon: , + isHiddenFn: (user) => user.isAdmin, + onClick: async (user) => { + setConfirmationHeader(<>Make Admin) + setConfirmationBody( + <> + Are you sure you want to make{' '} + + {userToString(user)} + {' '} + admin? + , + ) + setConfirmationRequest(() => async () => { + await UserAPI.makeAdmin(user.id, { makeAdmin: true }) + await mutate() + }) + setIsConfirmationDestructive(false) + setIsConfirmationOpen(true) + }, + }, + { + label: 'Demote Admin', + icon: , + isDestructive: true, + isHiddenFn: (user) => !user.isAdmin, + onClick: async (user) => { + setConfirmationHeader(<>Demote Admin) + setConfirmationBody( + <> + Are you sure you want to demote{' '} + + {userToString(user)} + + ? + , + ) + setConfirmationRequest(() => async () => { + await UserAPI.makeAdmin(user.id, { makeAdmin: false }) + await mutate() + if (getUserId() === user.id) { + navigate('/sign-out') + } + }) + setIsConfirmationDestructive(true) + setIsConfirmationOpen(true) + }, + }, + ]} + pagination={ + list.totalPages > 1 ? ( + - - ) : ( - - )} - + ) : undefined + } + /> ) : null}
{confirmationHeader && confirmationBody && confirmationRequest ? ( diff --git a/ui/src/pages/console/console-panel-workspaces.tsx b/ui/src/pages/console/console-panel-workspaces.tsx index cde55f44a..bd29db793 100644 --- a/ui/src/pages/console/console-panel-workspaces.tsx +++ b/ui/src/pages/console/console-panel-workspaces.tsx @@ -51,13 +51,17 @@ const ConsolePanelWorkspaces = () => { const [workspaceId, setWorkspaceId] = useState() const { data: list, - error, + error: listError, mutate, } = ConsoleAPI.useListOrSearchObject( 'workspace', { page, size, query }, swrConfig(), ) + const isListLoading = !list && !listError + const isListError = !list && listError + const isListEmpty = list && !listError && list.totalElements === 0 + const isListReady = list && !listError && list.totalElements > 0 const renameRequest = useCallback( async (name: string) => { @@ -76,101 +80,94 @@ const ConsolePanelWorkspaces = () => {
Workspaces - {!list && error ? ( + {isListLoading ? : null} + {isListError ? ( ) : null} - {!list && !error ? : null} - {list && !error ? ( - <> - {list.totalElements > 0 ? ( - <> - ( -
- + {isListEmpty ? ( + + ) : null} + {isListReady ? ( + ( +
+ - {workspace.name} -
- ), - }, - { - title: 'Organization', - renderCell: (workspace) => ( - - - {workspace.organization.name} - - - ), - }, - { - title: 'Quota', - renderCell: (workspace) => ( - {prettyBytes(workspace.storageCapacity)} - ), - }, - { - title: 'Created', - renderCell: (workspace) => ( - - ), - }, - { - title: 'Updated', - renderCell: (workspace) => ( - - ), - }, - ]} - actions={[ - { - label: 'Edit Name', - icon: , - onClick: async (workspace) => { - setCurrentName(workspace.name) - setWorkspaceId(workspace.id) - setIsConfirmRenameOpen(true) - }, - }, - ]} - pagination={ - list.totalPages > 1 ? ( - - ) : undefined - } + {workspace.name} +
+ ), + }, + { + title: 'Organization', + renderCell: (workspace) => ( + + {workspace.organization.name} + + ), + }, + { + title: 'Quota', + renderCell: (workspace) => ( + {prettyBytes(workspace.storageCapacity)} + ), + }, + { + title: 'Created', + renderCell: (workspace) => ( + + ), + }, + { + title: 'Updated', + renderCell: (workspace) => ( + + ), + }, + ]} + actions={[ + { + label: 'Edit Name', + icon: , + onClick: async (workspace) => { + setCurrentName(workspace.name) + setWorkspaceId(workspace.id) + setIsConfirmRenameOpen(true) + }, + }, + ]} + pagination={ + list.totalPages > 1 ? ( + - - ) : ( - - )} - + ) : undefined + } + /> ) : null}
{ const navigate = useNavigate() const dispatch = useAppDispatch() const { id } = useParams() - const { data: group, mutate } = GroupAPI.useGet(id, swrConfig()) + const { + data: group, + error: groupError, + mutate, + } = GroupAPI.useGet(id, swrConfig()) const [tabIndex, setTabIndex] = useState(0) + const isGroupLoading = !group && !groupError + const isGroupError = !group && groupError + const isGroupReady = group && !groupError useEffect(() => { const segments = location.pathname.split('/') @@ -41,29 +49,33 @@ const GroupLayout = () => { } }, [mutate, dispatch]) - if (!group) { - return null - } - return ( <> - - {group.name} - -
- - {group.name} - - - - navigate(`/group/${id}/member`)}>Members - navigate(`/group/${id}/settings`)}> - Settings - - - - -
+ {isGroupLoading ? : null} + {isGroupError ? : null} + {isGroupReady ? ( + <> + + {group.name} + +
+ + {group.name} + + + + navigate(`/group/${id}/member`)}> + Members + + navigate(`/group/${id}/settings`)}> + Settings + + + + +
+ + ) : null} ) } diff --git a/ui/src/pages/group/group-list-page.tsx b/ui/src/pages/group/group-list-page.tsx index 08b1e9815..7db0b3684 100644 --- a/ui/src/pages/group/group-list-page.tsx +++ b/ui/src/pages/group/group-list-page.tsx @@ -48,12 +48,16 @@ const GroupListPage = () => { }) const { data: list, - error, + error: listError, mutate, } = GroupAPI.useList( { query, page, size, sortOrder: SortOrder.Desc }, swrConfig(), ) + const isListLoading = !list && !listError + const isListError = !list && listError + const isListEmpty = list && !listError && list.totalElements === 0 + const isListReady = list && !listError && list.totalElements > 0 useEffect(() => { mutate().then() @@ -72,98 +76,81 @@ const GroupListPage = () => {
Groups - {error ? : null} - {!list && !error ? : null} - {list && list.totalElements === 0 ? ( -
-
- There are no groups. - -
-
+ {isListLoading ? : null} + {isListError ? : null} + {isListEmpty ? ( + } + /> ) : null} - {list && !error ? ( - <> - {list.totalElements > 0 ? ( - <> - ( -
- - - {g.name} - -
- ), - }, - { - title: 'Organization', - renderCell: (g) => ( - - {g.organization.name} - - ), - }, - { - title: 'Permission', - renderCell: (g) => {g.permission}, - }, - { - title: 'Date', - renderCell: (g) => ( - - ), - }, - ]} - pagination={ - list.totalPages > 1 ? ( - - ) : undefined - } + {isListReady ? ( + ( +
+ + + {g.name} + +
+ ), + }, + { + title: 'Organization', + renderCell: (g) => ( + + {g.organization.name} + + ), + }, + { + title: 'Permission', + renderCell: (g) => {g.permission}, + }, + { + title: 'Date', + renderCell: (g) => ( + + ), + }, + ]} + pagination={ + list.totalPages > 1 ? ( + - - ) : ( - - )} - + ) : undefined + } + /> ) : null}
diff --git a/ui/src/pages/group/group-members-page.tsx b/ui/src/pages/group/group-members-page.tsx index 5b62a2720..5f0e760c4 100644 --- a/ui/src/pages/group/group-members-page.tsx +++ b/ui/src/pages/group/group-members-page.tsx @@ -74,6 +74,13 @@ const GroupMembersPage = () => { const [isAddMembersModalOpen, setIsAddMembersModalOpen] = useState(false) const [isRemoveMemberModalOpen, setIsRemoveMemberModalOpen] = useState(false) + const isGroupLoading = !group && !groupError + const isGroupError = !group && groupError + const isGroupReady = group && !groupError + const isListLoading = !list && !listError + const isListError = !list && listError + const isListEmpty = list && !listError && list.totalElements === 0 + const isListReady = list && !listError && list.totalElements > 0 useEffect(() => { if (mutate) { @@ -83,103 +90,93 @@ const GroupMembersPage = () => { return ( <> - {!group && groupError ? ( - - ) : null} - {!group && !groupError ? : null} - {group && !groupError ? ( + {isGroupLoading ? : null} + {isGroupError ? : null} + {isGroupReady ? ( <> - {!list && listError ? ( - + {isListLoading ? : null} + {isListError ? : null} + {isListEmpty ? ( + } + onClick={() => { + setIsAddMembersModalOpen(true) + }} + > + Add Members + + ) : undefined + } + /> ) : null} - {list && !listError ? ( - <> - {list.totalElements > 0 ? ( - <> - ( -
- - {truncateEnd(u.fullName, 50)} -
- ), - }, - { - title: 'Email', - renderCell: (u) => ( - {truncateMiddle(u.email, 50)} - ), - }, - ]} - actions={[ - { - label: 'Remove From Group', - icon: , - isDestructive: true, - onClick: (u) => { - setUserToRemove(u) - setIsRemoveMemberModalOpen(true) - }, - }, - ]} - pagination={ - list.totalPages > 1 ? ( - - ) : undefined - } + {isListReady ? ( + ( +
+ + {truncateEnd(u.fullName, 50)} +
+ ), + }, + { + title: 'Email', + renderCell: (u) => {truncateMiddle(u.email, 50)}, + }, + ]} + actions={[ + { + label: 'Remove From Group', + icon: , + isDestructive: true, + onClick: (u) => { + setUserToRemove(u) + setIsRemoveMemberModalOpen(true) + }, + }, + ]} + pagination={ + list.totalPages > 1 ? ( + - - ) : ( - } - onClick={() => { - setIsAddMembersModalOpen(true) - }} - > - Add Members - - ) : undefined - } - /> - )} - + ) : undefined + } + /> ) : null} {userToRemove ? ( { const { id } = useParams() - const { data: group, error } = GroupAPI.useGet(id, swrConfig()) + const { data: group, error: groupError } = GroupAPI.useGet(id, swrConfig()) const [isNameModalOpen, setIsNameModalOpen] = useState(false) const [isAddMembersModalOpen, setIsAddMembersModalOpen] = useState(false) const [deleteModalOpen, setDeleteModalOpen] = useState(false) @@ -40,12 +40,15 @@ const GroupSettingsPage = () => { () => group && geOwnerPermission(group.permission), [group], ) + const isGroupLoading = !group && !groupError + const isGroupError = !group && groupError + const isGroupReady = group && !groupError return ( <> - {!group && error ? : null} - {!group && !error ? : null} - {group && error ? ( + {isGroupLoading ? : null} + {isGroupError ? : null} + {isGroupReady ? ( <> Date: Wed, 13 Nov 2024 07:48:13 +0100 Subject: [PATCH 08/13] wip(ui): error handling for organizations --- .../organization-invitations-page.tsx | 151 +++++++-------- .../organization/organization-layout.tsx | 66 ++++--- .../organization/organization-list-page.tsx | 135 +++++++------ .../organization-members-page.tsx | 180 +++++++++--------- .../organization-settings-page.tsx | 13 +- 5 files changed, 277 insertions(+), 268 deletions(-) diff --git a/ui/src/pages/organization/organization-invitations-page.tsx b/ui/src/pages/organization/organization-invitations-page.tsx index fab097563..53d7d9c89 100644 --- a/ui/src/pages/organization/organization-invitations-page.tsx +++ b/ui/src/pages/organization/organization-invitations-page.tsx @@ -62,6 +62,13 @@ const OrganizationInvitationsPage = () => { ) const [isInviteMembersModalOpen, setIsInviteMembersModalOpen] = useState(false) + const isOrgLoading = !org && !orgError + const isOrgError = !org && orgError + const isOrgReady = org && !orgError + const isListLoading = !list && !listError + const isListError = !list && listError + const isListEmpty = list && !listError && list.totalElements === 0 + const isListReady = list && !listError && list.totalElements > 0 const handleResend = useCallback( async (invitationId: string) => { @@ -91,87 +98,77 @@ const OrganizationInvitationsPage = () => { return ( <> - {!org && orgError ? ( - - ) : null} - {org && !orgError ? ( + {isOrgLoading ? : null} + {isOrgError ? : null} + {isOrgReady ? ( <> - {!list && listError && org && !orgError ? ( + {isListLoading ? : null} + {isListError ? ( ) : null} - {!list && !listError ? : null} - {list && !listError ? ( - <> - {list.totalElements > 0 ? ( - <> - ( - {truncateMiddle(i.email, 50)} - ), - }, - { - title: 'Status', - renderCell: (i) => ( - - ), - }, - { - title: 'Date', - renderCell: (i) => ( - - ), - }, - ]} - actions={[ - { - label: 'Resend', - icon: , - onClick: (i) => handleResend(i.id), - }, - { - label: 'Delete', - icon: , - isDestructive: true, - onClick: (i) => handleDelete(i.id), - }, - ]} - pagination={ - list.totalPages ? ( - - ) : undefined - } + {isListEmpty ? ( + } + onClick={() => { + setIsInviteMembersModalOpen(true) + }} + > + Invite Members + + ) : undefined + } + /> + ) : null} + {isListReady ? ( + {truncateMiddle(i.email, 50)}, + }, + { + title: 'Status', + renderCell: (i) => , + }, + { + title: 'Date', + renderCell: (i) => ( + + ), + }, + ]} + actions={[ + { + label: 'Resend', + icon: , + onClick: (i) => handleResend(i.id), + }, + { + label: 'Delete', + icon: , + isDestructive: true, + onClick: (i) => handleDelete(i.id), + }, + ]} + pagination={ + list.totalPages ? ( + - - ) : ( - } - onClick={() => { - setIsInviteMembersModalOpen(true) - }} - > - Invite Members - - ) : undefined - } - /> - )} - + ) : undefined + } + /> ) : null} { const navigate = useNavigate() const dispatch = useAppDispatch() const { id } = useParams() - const { data: org, mutate } = OrganizationAPI.useGet(id, swrConfig()) + const { + data: org, + error: orgError, + mutate, + } = OrganizationAPI.useGet(id, swrConfig()) const [tabIndex, setTabIndex] = useState(0) + const isOrgLoading = !org && !orgError + const isOrgError = !org && orgError + const isOrgReady = org && !orgError useEffect(() => { const segments = location.pathname.split('/') @@ -50,31 +58,37 @@ const OrganizationLayout = () => { return ( <> - - {org.name} - -
- - {org.name} - - - - navigate(`/organization/${id}/member`)}> - Members - - navigate(`/organization/${id}/invitation`)} - display={geOwnerPermission(org.permission) ? 'auto' : 'none'} - > - Invitations - - navigate(`/organization/${id}/settings`)}> - Settings - - - - -
+ {isOrgLoading ? : null} + {isOrgError ? : null} + {isOrgReady ? ( + <> + + {org.name} + +
+ + {org.name} + + + + navigate(`/organization/${id}/member`)}> + Members + + navigate(`/organization/${id}/invitation`)} + display={geOwnerPermission(org.permission) ? 'auto' : 'none'} + > + Invitations + + navigate(`/organization/${id}/settings`)}> + Settings + + + + +
+ + ) : null} ) } diff --git a/ui/src/pages/organization/organization-list-page.tsx b/ui/src/pages/organization/organization-list-page.tsx index 28df4784e..985098926 100644 --- a/ui/src/pages/organization/organization-list-page.tsx +++ b/ui/src/pages/organization/organization-list-page.tsx @@ -48,12 +48,16 @@ const OrganizationListPage = () => { }) const { data: list, - error, + error: listError, mutate, } = OrganizationAPI.useList( { query, page, size, sortOrder: SortOrder.Desc }, swrConfig(), ) + const isListLoading = !list && !listError + const isListError = !list && listError + const isListEmpty = list && !listError && list.totalElements === 0 + const isListReady = list && !listError && list.totalElements > 0 useEffect(() => { mutate().then() @@ -72,76 +76,71 @@ const OrganizationListPage = () => {
Organizations - {!list && error ? ( + {isListLoading ? : null} + {isListError ? ( ) : null} - {!list && !error ? : null} - {list && !error ? ( - <> - {list.totalElements > 0 ? ( - <> - ( -
- - - {o.name} - -
- ), - }, - { - title: 'Permission', - renderCell: (o) => {o.permission}, - }, - { - title: 'Date', - renderCell: (o) => ( - - ), - }, - ]} - pagination={ - list.totalPages > 1 ? ( - - ) : undefined - } + {isListEmpty ? ( + } + /> + ) : null} + {isListReady ? ( + ( +
+ + + {o.name} + +
+ ), + }, + { + title: 'Permission', + renderCell: (o) => {o.permission}, + }, + { + title: 'Date', + renderCell: (o) => ( + + ), + }, + ]} + pagination={ + list.totalPages > 1 ? ( + - - ) : ( - } - /> - )} - + ) : undefined + } + /> ) : null}
diff --git a/ui/src/pages/organization/organization-members-page.tsx b/ui/src/pages/organization/organization-members-page.tsx index 6ed267770..bf8904712 100644 --- a/ui/src/pages/organization/organization-members-page.tsx +++ b/ui/src/pages/organization/organization-members-page.tsx @@ -78,103 +78,101 @@ const OrganizationMembersPage = () => { const [userToRemove, setUserToRemove] = useState() const [isRemoveMemberModalOpen, setIsRemoveMemberModalOpen] = useState(false) + const isOrgLoading = !org && !orgError + const isOrgError = !org && orgError + const isOrgReady = org && !orgError + const isListLoading = !list && !listError + const isListError = !list && listError + const isListEmpty = list && !listError && list.totalElements === 0 + const isListReady = list && !listError && list.totalElements > 0 return ( <> - {!org && orgError ? ( - - ) : null} - {!org && !orgError ? : null} - {org && !orgError ? ( + {isOrgLoading ? : null} + {isOrgError ? : null} + {isOrgReady ? ( <> - {!list && listError ? ( - + {isListLoading ? : null} + {isListError ? : null} + {isListEmpty ? ( + } + onClick={() => dispatch(inviteModalDidOpen())} + > + Invite Members + + ) : undefined + } + /> ) : null} - {!list && !listError ? : null} - {list && !listError ? ( - <> - {list.totalElements > 0 ? ( - ( -
- - {truncateEnd(u.fullName, 50)} -
- ), - }, - { - title: 'Email', - renderCell: (u) => ( - {truncateMiddle(u.email, 50)} - ), - }, - ]} - actions={[ - { - label: 'Remove From Organization', - icon: , - isDestructive: true, - onClick: (u) => { - setUserToRemove(u) - setIsRemoveMemberModalOpen(true) - }, - }, - ]} - pagination={ - list.totalPages > 1 ? ( - ( +
+ - ) : undefined - } - /> - ) : ( - } - onClick={() => dispatch(inviteModalDidOpen())} - > - Invite Members - - ) : undefined - } - /> - )} - + {truncateEnd(u.fullName, 50)} +
+ ), + }, + { + title: 'Email', + renderCell: (u) => {truncateMiddle(u.email, 50)}, + }, + ]} + actions={[ + { + label: 'Remove From Organization', + icon: , + isDestructive: true, + onClick: (u) => { + setUserToRemove(u) + setIsRemoveMemberModalOpen(true) + }, + }, + ]} + pagination={ + list.totalPages > 1 ? ( + + ) : undefined + } + /> ) : null} {userToRemove ? ( { const { id } = useParams() - const { data: org, error } = OrganizationAPI.useGet(id, swrConfig()) + const { data: org, error: orgError } = OrganizationAPI.useGet(id, swrConfig()) const [isNameModalOpen, setIsNameModalOpen] = useState(false) const [isInviteMembersModalOpen, setIsInviteMembersModalOpen] = useState(false) const [isLeaveModalOpen, setIsLeaveModalOpen] = useState(false) const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false) + const isOrgLoading = !org && !orgError + const isOrgError = !org && orgError + const isOrgReady = org && !orgError return ( <> - {!org && error ? ( - - ) : null} - {!org && !error ? : null} - {org && !error ? ( + {isOrgLoading ? : null} + {isOrgError ? : null} + {isOrgReady ? ( <> Date: Wed, 13 Nov 2024 08:53:27 +0100 Subject: [PATCH 09/13] wip(ui): error handling for workspaces --- ui/src/components/file/list/index.tsx | 13 +- .../pages/account/account-settings-page.tsx | 283 +++++++++--------- .../pages/workspace/workspace-files-page.tsx | 185 ++++++------ ui/src/pages/workspace/workspace-layout.tsx | 64 ++-- .../pages/workspace/workspace-list-page.tsx | 170 +++++------ .../workspace/workspace-settings-page.tsx | 34 ++- 6 files changed, 385 insertions(+), 364 deletions(-) diff --git a/ui/src/components/file/list/index.tsx b/ui/src/components/file/list/index.tsx index a4c0d243c..caf120391 100644 --- a/ui/src/components/file/list/index.tsx +++ b/ui/src/components/file/list/index.tsx @@ -9,6 +9,7 @@ // licenses/AGPL.txt. import { MouseEvent, useCallback, useEffect, useMemo, useState } from 'react' import { useParams } from 'react-router-dom' +import { SectionPlaceholder, SectionSpinner } from '@koupr/ui' import { DndContext, useSensors, @@ -229,17 +230,7 @@ const FileList = ({ list, scale }: FileListProps) => { onDragEnd={handleDragEnd} > {list.totalElements === 0 ? ( -
- There are no items. -
+ ) : null} {viewType === FileViewType.Grid && list.totalElements > 0 ? (
{ {isUserError ? : null} {isUserReady ? ( <> - {isStorageUsageLoading ? : null} - {isStorageUsageError ? ( - - ) : null} - {isStorageUsageReady ? ( - - {storageUsageError ? ( - Failed to load storage usage. - ) : null} - {storageUsage && !storageUsageError ? ( - <> + + {isStorageUsageError ? ( + + ) : null} + {isStorageUsageReady ? ( + <> + + {prettyBytes(storageUsage.bytes)} of{' '} + {prettyBytes(storageUsage.maxBytes)} used + + + + ) : null} + {isStorageUsageLoading ? ( + <> + Calculating… + + + ) : null} + + ), + }, + { + title: 'Basics', + rows: [ + { + label: 'Full name', + content: ( + <> + {truncateEnd(user.fullName, 50)} + setIsFullNameModalOpen(true)} + /> + + ), + }, + ], + }, + { + title: 'Credentials', + rows: [ + { + label: 'Email', + content: ( + <> + {user.pendingEmail ? ( +
+ +
+ +
+
+ {truncateMiddle(user.pendingEmail, 50)} +
+ ) : null} + {!user.pendingEmail ? ( - {prettyBytes(storageUsage.bytes)} of{' '} - {prettyBytes(storageUsage.maxBytes)} used + {truncateMiddle( + user.pendingEmail || user.email, + 50, + )} - - - ) : null} - {!(storageUsage && !storageUsageError) ? ( - <> - Calculating… - - - ) : null} - - ), - }, - { - title: 'Basics', - rows: [ - { - label: 'Full name', - content: ( - <> - {truncateEnd(user.fullName, 50)} - setIsFullNameModalOpen(true)} - /> - - ), - }, - ], - }, - { - title: 'Credentials', - rows: [ - { - label: 'Email', - content: ( - <> - {user.pendingEmail ? ( -
- -
- -
-
- - {truncateMiddle(user.pendingEmail, 50)} - -
- ) : null} - {!user.pendingEmail ? ( - - {truncateMiddle( - user.pendingEmail || user.email, - 50, - )} - - ) : null} - setIsEmailModalOpen(true)} - /> - - ), - }, - { - label: 'Password', - content: ( + ) : null} setIsPasswordModalOpen(true)} - /> - ), - }, - ], - }, - { - title: 'Theme', - rows: [ - { - label: 'Dark mode', - content: ( - toggleColorMode()} + title="Edit email" + aria-label="Edit email" + onClick={() => setIsEmailModalOpen(true)} /> - ), - }, - ], - }, - { - title: 'Advanced', - rows: [ - { - label: 'Delete account', - content: ( - } - variant="solid" - colorScheme="red" - title="Delete account" - aria-label="Delete account" - onClick={() => setIsDeleteModalOpen(true)} - /> - ), - }, - ], - }, - ]} - /> - ) : null} + + ), + }, + { + label: 'Password', + content: ( + setIsPasswordModalOpen(true)} + /> + ), + }, + ], + }, + { + title: 'Theme', + rows: [ + { + label: 'Dark mode', + content: ( + toggleColorMode()} + /> + ), + }, + ], + }, + { + title: 'Advanced', + rows: [ + { + label: 'Delete account', + content: ( + } + variant="solid" + colorScheme="red" + title="Delete account" + aria-label="Delete account" + onClick={() => setIsDeleteModalOpen(true)} + /> + ), + }, + ], + }, + ]} + /> { const isSearchFilterModalOpen = useAppSelector( (state) => state.ui.searchFilter.isModalOpen, ) - const { data: workspace } = WorkspaceAPI.useGet(workspaceId, swrConfig()) + const { + data: workspace, + error: workspaceError, + isLoading: isWorkspaceLoading, + } = WorkspaceAPI.useGet(workspaceId, swrConfig()) const { page, size, steps, setPage, setSize } = usePagePagination({ navigateFn: navigate, searchFn: () => location.search, @@ -91,8 +96,8 @@ const WorkspaceFilesPage = () => { }) const { data: list, - error, - isLoading, + error: listError, + isLoading: isListLoading, mutate, } = FileAPI.useList( fileId!, @@ -111,6 +116,10 @@ const WorkspaceFilesPage = () => { steps, }) const hasPagination = hasPageSwitcher || hasSizeSelector + const isWorkspaceError = !workspace && workspaceError + const isWorkspaceReady = workspace && !workspaceError + const isListError = !list && listError + const isListReady = list && !listError useEffect(() => { if (list) { @@ -126,97 +135,105 @@ const WorkspaceFilesPage = () => { return ( <> -
- {workspace && fileId ? ( - { - dispatch(selectionUpdated([])) - navigate(`/workspace/${workspace.id}/file/${fileId}`) - }} - /> - ) : null} - -
+ {isWorkspaceLoading ? ( +
+ +
+ ) : null} + {isWorkspaceError ? ( +
+ +
+ ) : null} + {isWorkspaceReady ? ( + <>
dispatch(selectionUpdated([]))} > - {isLoading ? ( + {workspace && fileId ? ( + { + dispatch(selectionUpdated([])) + navigate(`/workspace/${workspace.id}/file/${fileId}`) + }} + /> + ) : null} + +
dispatch(selectionUpdated([]))} > - + {isListLoading ? : null} + {isListError ? ( + + ) : null} + {isListReady ? ( + + ) : null}
- ) : null} - {list && !error ? : null} -
- {list && list.totalPages > 1 ? ( -
- + {list && list.totalPages > 1 ? ( +
+ +
+ ) : null}
- ) : null} -
-
- {list ? : null} - {isSnapshotListModalOpen ? : null} - {isSnapshotDetachModalOpen ? : null} - {isMoveModalOpen ? : null} - {isCopyModalOpen ? : null} - {isCreateModalOpen ? : null} - {isDeleteModalOpen ? : null} - {isRenameModalOpen ? : null} - {isInfoModalOpen ? : null} - {isInsightsModalOpen ? : null} - {isMosaicModalOpen ? : null} - {isSearchFilterModalOpen ? : null} +
+ {list ? : null} + {isSnapshotListModalOpen ? : null} + {isSnapshotDetachModalOpen ? : null} + {isMoveModalOpen ? : null} + {isCopyModalOpen ? : null} + {isCreateModalOpen ? : null} + {isDeleteModalOpen ? : null} + {isRenameModalOpen ? : null} + {isInfoModalOpen ? : null} + {isInsightsModalOpen ? : null} + {isMosaicModalOpen ? : null} + {isSearchFilterModalOpen ? : null} + + ) : null} ) } diff --git a/ui/src/pages/workspace/workspace-layout.tsx b/ui/src/pages/workspace/workspace-layout.tsx index b5a0c4e11..0d76c0e26 100644 --- a/ui/src/pages/workspace/workspace-layout.tsx +++ b/ui/src/pages/workspace/workspace-layout.tsx @@ -10,6 +10,7 @@ import { useEffect, useState } from 'react' import { Outlet, useLocation, useNavigate, useParams } from 'react-router-dom' import { Heading, Tab, TabList, Tabs } from '@chakra-ui/react' +import { SectionError, SectionSpinner } from '@koupr/ui' import cx from 'classnames' import { Helmet } from 'react-helmet-async' import WorkspaceAPI from '@/client/api/workspace' @@ -22,8 +23,15 @@ const WorkspaceLayout = () => { const { id } = useParams() const navigate = useNavigate() const dispatch = useAppDispatch() - const { data: workspace, mutate } = WorkspaceAPI.useGet(id, swrConfig()) + const { + data: workspace, + error: workspaceError, + isLoading: isWorkspaceLoading, + mutate, + } = WorkspaceAPI.useGet(id, swrConfig()) const [tabIndex, setTabIndex] = useState(0) + const isWorkspaceError = !workspace && workspaceError + const isWorkspaceReady = workspace && !workspaceError useEffect(() => { const segments = location.pathname.split('/') @@ -47,29 +55,37 @@ const WorkspaceLayout = () => { return ( <> - - {workspace.name} - -
- - {workspace.name} - - - - - navigate(`/workspace/${id}/file/${workspace.rootId}`) - } - > - Files - - navigate(`/workspace/${id}/settings`)}> - Settings - - - - -
+ {isWorkspaceLoading ? : null} + {isWorkspaceError ? ( + + ) : null} + {isWorkspaceReady ? ( + <> + + {workspace.name} + +
+ + {workspace.name} + + + + + navigate(`/workspace/${id}/file/${workspace.rootId}`) + } + > + Files + + navigate(`/workspace/${id}/settings`)}> + Settings + + + + +
+ + ) : null} ) } diff --git a/ui/src/pages/workspace/workspace-list-page.tsx b/ui/src/pages/workspace/workspace-list-page.tsx index a26dcbb5d..7b9c37d46 100644 --- a/ui/src/pages/workspace/workspace-list-page.tsx +++ b/ui/src/pages/workspace/workspace-list-page.tsx @@ -48,12 +48,16 @@ const WorkspaceListPage = () => { }) const { data: list, - error, + error: listError, + isLoading: isListLoading, mutate, } = WorkspaceAPI.useList( { query, page, size, sortOrder: SortOrder.Desc }, swrConfig(), ) + const isListError = !list && listError + const isListEmpty = list && !listError && list.totalElements === 0 + const isListReady = list && !listError && list.totalElements > 0 useEffect(() => { mutate().then() @@ -72,101 +76,83 @@ const WorkspaceListPage = () => {
Workspaces - {!list && error ? ( + {isListLoading ? : null} + {isListError ? ( ) : null} - {!list && !error ? : null} - {list && list.totalElements === 0 && !error ? ( -
-
- There are no workspaces. - -
-
+ {isListEmpty ? ( + } + /> ) : null} - {list && !error ? ( - <> - {list.totalElements > 0 ? ( - ( -
- - - {w.name} - -
- ), - }, - { - title: 'Organization', - renderCell: (w) => ( - - {w.organization.name} - - ), - }, - { - title: 'Permission', - renderCell: (w) => {w.permission}, - }, - { - title: 'Date', - renderCell: (w) => ( - - ), - }, - ]} - pagination={ - list.totalPages > 1 ? ( - ( +
+ - ) : undefined - } - /> - ) : ( - } - /> - )} - + + {w.name} + +
+ ), + }, + { + title: 'Organization', + renderCell: (w) => ( + + {w.organization.name} + + ), + }, + { + title: 'Permission', + renderCell: (w) => {w.permission}, + }, + { + title: 'Date', + renderCell: (w) => ( + + ), + }, + ]} + pagination={ + list.totalPages > 1 ? ( + + ) : undefined + } + /> ) : null}
diff --git a/ui/src/pages/workspace/workspace-settings-page.tsx b/ui/src/pages/workspace/workspace-settings-page.tsx index 73fa48269..a028e3f1e 100644 --- a/ui/src/pages/workspace/workspace-settings-page.tsx +++ b/ui/src/pages/workspace/workspace-settings-page.tsx @@ -17,6 +17,7 @@ import { SectionError, SectionSpinner, } from '@koupr/ui' +import cx from 'classnames' import { geEditorPermission } from '@/client/api/permission' import StorageAPI from '@/client/api/storage' import WorkspaceAPI from '@/client/api/workspace' @@ -35,12 +36,14 @@ const WorkspaceSettingsPage = () => { const { id } = useParams() const { data: workspace, - error, + error: workspaceError, + isLoading: isWorkspaceLoading, mutate, } = WorkspaceAPI.useGet(id, swrConfig()) const { data: storageUsage, error: storageUsageError, + isLoading: isStorageUsageLoading, mutate: mutateStorageUsage, } = StorageAPI.useGetWorkspaceUsage(id, swrConfig()) const hasEditPermission = useMemo( @@ -51,6 +54,10 @@ const WorkspaceSettingsPage = () => { const [isStorageCapacityModalOpen, setIsStorageCapacityModalOpen] = useState(false) const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false) + const isWorkspaceError = !workspace && workspaceError + const isWorkspaceReady = workspace && !workspaceError + const isStorageUsageError = !storageUsage && storageUsageError + const isStorageUsageReady = storageUsage && !storageUsageError const handleEditNameClose = useCallback(async () => { setIsNameModalOpen(false) @@ -64,11 +71,17 @@ const WorkspaceSettingsPage = () => { return ( <> - {!workspace && error ? ( - + {isWorkspaceLoading ? ( +
+ +
) : null} - {!workspace && !error ? : null} - {workspace && !error ? ( + {isWorkspaceError ? ( +
+ +
+ ) : null} + {isWorkspaceReady ? ( <> { title: 'Storage', content: ( <> - {storageUsageError ? ( - Failed to load storage usage. + {isStorageUsageError ? ( + ) : null} - {storageUsage && !storageUsageError ? ( + {isStorageUsageReady ? ( <> {prettyBytes(storageUsage.bytes)} of{' '} @@ -88,7 +104,7 @@ const WorkspaceSettingsPage = () => { ) : null} - {!storageUsage && !storageUsageError ? ( + {isStorageUsageLoading ? ( <> Calculating… From 12b4c2739ed1347a70a7b0d3a224e81f883d92df Mon Sep 17 00:00:00 2001 From: Anass Bouassaba Date: Wed, 13 Nov 2024 20:43:10 +0100 Subject: [PATCH 10/13] refactor(ui): use isLoading from SWR --- ui/src/components/common/group-selector.tsx | 2 +- .../common/organization-selector.tsx | 2 +- ui/src/components/common/user-selector.tsx | 2 +- ui/src/components/insights/index.tsx | 9 ++- .../components/insights/insights-create.tsx | 15 ++-- .../insights-overview-artifacts.tsx | 7 +- .../insights-overview-chart.tsx | 7 +- .../insights-overview-entities.tsx | 2 +- .../insights-overview-settings.tsx | 4 +- ui/src/components/mosaic/index.tsx | 9 ++- .../mosaic/mosaic-overview/index.tsx | 7 +- .../mosaic-overview-artifacts.tsx | 7 +- .../mosaic-overview-settings.tsx | 4 +- ui/src/components/snapshot/snapshot-list.tsx | 2 +- ui/src/components/task/task-drawer.tsx | 2 +- ui/src/components/task/task-list.tsx | 2 +- .../workspace-edit-storage-capacity.tsx | 8 +- .../account/account-invitations-page.tsx | 9 ++- ui/src/pages/account/account-layout.tsx | 8 +- .../pages/account/account-settings-page.tsx | 15 ++-- ui/src/pages/console/console-panel-groups.tsx | 2 +- .../console/console-panel-invitations.tsx | 2 +- .../console/console-panel-organization.tsx | 73 +++++++++++-------- .../console/console-panel-organizations.tsx | 2 +- ui/src/pages/console/console-panel-user.tsx | 58 +++++++++------ ui/src/pages/console/console-panel-users.tsx | 2 +- .../console/console-panel-workspaces.tsx | 2 +- ui/src/pages/group/group-layout.tsx | 2 +- ui/src/pages/group/group-list-page.tsx | 2 +- ui/src/pages/group/group-members-page.tsx | 9 ++- ui/src/pages/group/group-settings-page.tsx | 7 +- .../organization-invitations-page.tsx | 9 ++- .../organization/organization-layout.tsx | 2 +- .../organization/organization-list-page.tsx | 2 +- .../organization-members-page.tsx | 9 ++- .../organization-settings-page.tsx | 7 +- 36 files changed, 192 insertions(+), 121 deletions(-) diff --git a/ui/src/components/common/group-selector.tsx b/ui/src/components/common/group-selector.tsx index 2d7bd4d40..23cd8c990 100644 --- a/ui/src/components/common/group-selector.tsx +++ b/ui/src/components/common/group-selector.tsx @@ -54,12 +54,12 @@ const GroupSelector = ({ const { data: list, error: listError, + isLoading: isListLoading, mutate, } = GroupAPI.useList( { query, organizationId, page, size: 5, sortOrder: SortOrder.Desc }, swrConfig(), ) - const isListLoading = !list && !listError const isListError = !list && listError const isListEmpty = list && !listError && list.totalElements === 0 const isListReady = list && !listError && list.totalElements > 0 diff --git a/ui/src/components/common/organization-selector.tsx b/ui/src/components/common/organization-selector.tsx index e59d07766..e34c852e9 100644 --- a/ui/src/components/common/organization-selector.tsx +++ b/ui/src/components/common/organization-selector.tsx @@ -52,12 +52,12 @@ const OrganizationSelector = ({ onConfirm }: OrganizationSelectorProps) => { const { data: list, error: listError, + isLoading: isListLoading, mutate, } = OrganizationAPI.useList( { query, page, size: 5, sortOrder: SortOrder.Desc }, swrConfig(), ) - const isListLoading = !list && !listError const isListError = !list && listError const isListEmpty = list && !listError && list.totalElements === 0 const isListReady = list && !listError && list.totalElements > 0 diff --git a/ui/src/components/common/user-selector.tsx b/ui/src/components/common/user-selector.tsx index 71908c0e9..16e5f1622 100644 --- a/ui/src/components/common/user-selector.tsx +++ b/ui/src/components/common/user-selector.tsx @@ -60,6 +60,7 @@ const UserSelector = ({ const { data: list, error: listError, + isLoading: isListLoading, mutate, } = UserAPI.useList( { @@ -73,7 +74,6 @@ const UserSelector = ({ }, swrConfig(), ) - const isListLoading = !list && !listError const isListError = !list && listError const isListEmpty = list && !listError && list.totalElements === 0 const isListReady = list && !listError && list.totalElements > 0 diff --git a/ui/src/components/insights/index.tsx b/ui/src/components/insights/index.tsx index 22169872e..bbcd9e036 100644 --- a/ui/src/components/insights/index.tsx +++ b/ui/src/components/insights/index.tsx @@ -35,13 +35,16 @@ const Insights = () => { const { data: info, error: infoError, + isLoading: isInfoLoading, mutate: mutateInfo, } = InsightsAPI.useGetInfo(id, swrConfig()) - const { data: file, error: fileError } = FileAPI.useGet(id, swrConfig()) - const isInfoLoading = !info && !infoError + const { + data: file, + error: fileError, + isLoading: isFileLoading, + } = FileAPI.useGet(id, swrConfig()) const isInfoError = !info && infoError const isInfoReady = info && !infoError - const isFileLoading = !file && !fileError const isFileError = !file && fileError const isFileReady = file && !fileError diff --git a/ui/src/components/insights/insights-create.tsx b/ui/src/components/insights/insights-create.tsx index 1abd4009e..362bce2ff 100644 --- a/ui/src/components/insights/insights-create.tsx +++ b/ui/src/components/insights/insights-create.tsx @@ -35,9 +35,16 @@ const InsightsCreate = () => { const mutateTasks = useAppSelector((state) => state.ui.tasks.mutateList) const mutateInfo = useAppSelector((state) => state.ui.insights.mutateInfo) const [language, setLanguage] = useState() - const { data: languages, error: languagesError } = - InsightsAPI.useGetLanguages(swrConfig()) - const { data: file, error: fileError } = FileAPI.useGet(id, swrConfig()) + const { + data: languages, + error: languagesError, + isLoading: isLanguagesLoading, + } = InsightsAPI.useGetLanguages(swrConfig()) + const { + data: file, + error: fileError, + isLoading: isFileLoading, + } = FileAPI.useGet(id, swrConfig()) const existingLanguage = useMemo(() => { if (file && languages && file.snapshot?.language) { const value = file.snapshot.language @@ -49,10 +56,8 @@ const InsightsCreate = () => { } } }, [file, languages]) - const isFileLoading = !file && !fileError const isFileError = !file && fileError const isFileReady = file && !fileError - const isLanguagesLoading = !languages && !languagesError const isLanguagesError = !languages && languagesError const isLanguagesReady = languages && !languagesError diff --git a/ui/src/components/insights/insights-overview/insights-overview-artifacts.tsx b/ui/src/components/insights/insights-overview/insights-overview-artifacts.tsx index 49ecf60ad..989e112c3 100644 --- a/ui/src/components/insights/insights-overview/insights-overview-artifacts.tsx +++ b/ui/src/components/insights/insights-overview/insights-overview-artifacts.tsx @@ -21,11 +21,14 @@ const InsightsOverviewArtifacts = () => { ? state.ui.files.selection[0] : undefined, ) - const { data: file, error: fileError } = FileAPI.useGet(id, swrConfig()) + const { + data: file, + error: fileError, + isLoading: isFileLoading, + } = FileAPI.useGet(id, swrConfig()) const searchParams = new URLSearchParams({ access_token: getAccessTokenOrRedirect(), }) - const isFileLoading = !file && !fileError const isFileError = !file && fileError const isFileReady = file && !fileError diff --git a/ui/src/components/insights/insights-overview/insights-overview-chart.tsx b/ui/src/components/insights/insights-overview/insights-overview-chart.tsx index 6436c13db..e35c1e1d1 100644 --- a/ui/src/components/insights/insights-overview/insights-overview-chart.tsx +++ b/ui/src/components/insights/insights-overview/insights-overview-chart.tsx @@ -22,12 +22,15 @@ const InsightsOverviewChart = () => { ? state.ui.files.selection[0] : undefined, ) - const { data: list, error: listError } = InsightsAPI.useListEntities( + const { + data: list, + error: listError, + isLoading: isListLoading, + } = InsightsAPI.useListEntities( id, { page: 1, size: 5, sortBy: SortBy.Frequency, sortOrder: SortOrder.Desc }, swrConfig(), ) - const isListLoading = !list && !listError const isListError = !list && listError const isListEmpty = list && !listError && list.totalElements < 5 const isListReady = list && !listError && list.totalElements >= 5 diff --git a/ui/src/components/insights/insights-overview/insights-overview-entities.tsx b/ui/src/components/insights/insights-overview/insights-overview-entities.tsx index 9c0cfb2cb..e91f85095 100644 --- a/ui/src/components/insights/insights-overview/insights-overview-entities.tsx +++ b/ui/src/components/insights/insights-overview/insights-overview-entities.tsx @@ -33,6 +33,7 @@ const InsightsOverviewEntities = () => { const { data: list, error: listError, + isLoading: isListLoading, mutate, } = InsightsAPI.useListEntities( metadata ? id : undefined, @@ -45,7 +46,6 @@ const InsightsOverviewEntities = () => { }, query ? undefined : swrConfig(), ) - const isListLoading = !list && !listError const isListError = !list && listError const isListEmpty = list && !listError && list.totalElements === 0 const isListReady = list && !listError && list.totalElements > 0 diff --git a/ui/src/components/insights/insights-overview/insights-overview-settings.tsx b/ui/src/components/insights/insights-overview/insights-overview-settings.tsx index d83f6d4bc..f3ffb669d 100644 --- a/ui/src/components/insights/insights-overview/insights-overview-settings.tsx +++ b/ui/src/components/insights/insights-overview/insights-overview-settings.tsx @@ -35,11 +35,13 @@ const InsightsOverviewSettings = () => { const { data: info, error: infoError, + isLoading: isInfoLoading, mutate: mutateInfo, } = InsightsAPI.useGetInfo(id, swrConfig()) const { data: file, error: fileError, + isLoading: isFileLoading, mutate: mutateFile, } = FileAPI.useGet(id, swrConfig()) const canCollect = useMemo(() => { @@ -57,10 +59,8 @@ const InsightsOverviewSettings = () => { geOwnerPermission(file?.permission ?? NONE_PERMISSION) ) }, [info, file]) - const isFileLoading = !file && !fileError const isFileError = !file && fileError const isFileReady = file && !fileError - const isInfoLoading = !info && !infoError const isInfoError = !info && infoError const isInfoReady = info && !infoError diff --git a/ui/src/components/mosaic/index.tsx b/ui/src/components/mosaic/index.tsx index 363761b10..17ba55d34 100644 --- a/ui/src/components/mosaic/index.tsx +++ b/ui/src/components/mosaic/index.tsx @@ -36,13 +36,16 @@ const Mosaic = () => { const { data: info, error: infoError, + isLoading: isInfoLoading, mutate: mutateInfo, } = MosaicAPI.useGetInfo(id, swrConfig()) - const { data: file, error: fileError } = FileAPI.useGet(id, swrConfig()) - const isFileLoading = !file && !fileError + const { + data: file, + error: fileError, + isLoading: isFileLoading, + } = FileAPI.useGet(id, swrConfig()) const isFileError = !file && fileError const isFileReady = file && !fileError - const isInfoLoading = !info && !infoError const isInfoError = !info && infoError const isInfoReady = info && !infoError diff --git a/ui/src/components/mosaic/mosaic-overview/index.tsx b/ui/src/components/mosaic/mosaic-overview/index.tsx index 2a1eeaf83..2c958f9d5 100644 --- a/ui/src/components/mosaic/mosaic-overview/index.tsx +++ b/ui/src/components/mosaic/mosaic-overview/index.tsx @@ -36,8 +36,11 @@ const MosaicOverview = () => { : undefined, ) const [isWarningVisible, setIsWarningVisible] = useState(true) - const { data: info, error: infoError } = MosaicAPI.useGetInfo(id, swrConfig()) - const isInfoLoading = !info && !infoError + const { + data: info, + error: infoError, + isLoading: isInfoLoading, + } = MosaicAPI.useGetInfo(id, swrConfig()) const isInfoError = !info && infoError const isInfoReady = info && !infoError diff --git a/ui/src/components/mosaic/mosaic-overview/mosaic-overview-artifacts.tsx b/ui/src/components/mosaic/mosaic-overview/mosaic-overview-artifacts.tsx index e6b7e910e..77091839c 100644 --- a/ui/src/components/mosaic/mosaic-overview/mosaic-overview-artifacts.tsx +++ b/ui/src/components/mosaic/mosaic-overview/mosaic-overview-artifacts.tsx @@ -20,8 +20,11 @@ const MosaicOverviewArtifacts = () => { ? state.ui.files.selection[0] : undefined, ) - const { data: file, error: fileError } = FileAPI.useGet(id, swrConfig()) - const isFileLoading = !file && !fileError + const { + data: file, + error: fileError, + isLoading: isFileLoading, + } = FileAPI.useGet(id, swrConfig()) const isFileError = !file && fileError const isFileReady = file && !fileError diff --git a/ui/src/components/mosaic/mosaic-overview/mosaic-overview-settings.tsx b/ui/src/components/mosaic/mosaic-overview/mosaic-overview-settings.tsx index 24e7cfabd..4ea862e7c 100644 --- a/ui/src/components/mosaic/mosaic-overview/mosaic-overview-settings.tsx +++ b/ui/src/components/mosaic/mosaic-overview/mosaic-overview-settings.tsx @@ -35,17 +35,17 @@ const MosaicOverviewSettings = () => { const { data: info, error: infoError, + isLoading: isInfoLoading, mutate: mutateInfo, } = MosaicAPI.useGetInfo(id, swrConfig()) const { data: file, error: fileError, + isLoading: isFileLoading, mutate: mutateFile, } = FileAPI.useGet(id, swrConfig()) - const isFileLoading = !file && !fileError const isFileError = !file && fileError const isFileReady = file && !fileError - const isInfoLoading = !info && !infoError const isInfoError = !info && infoError const isInfoReady = info && !infoError diff --git a/ui/src/components/snapshot/snapshot-list.tsx b/ui/src/components/snapshot/snapshot-list.tsx index 3ef064d04..3e996a9f5 100644 --- a/ui/src/components/snapshot/snapshot-list.tsx +++ b/ui/src/components/snapshot/snapshot-list.tsx @@ -57,6 +57,7 @@ const SnapshotList = () => { const { data: list, error: listError, + isLoading: isListLoading, mutate: snapshotMutate, } = SnapshotAPI.useList( { @@ -68,7 +69,6 @@ const SnapshotList = () => { }, swrConfig(), ) - const isListLoading = !list && !listError const isListError = !list && listError const isListEmpty = list && !listError && list.totalElements === 0 const isListReady = list && !listError && list.totalElements > 0 diff --git a/ui/src/components/task/task-drawer.tsx b/ui/src/components/task/task-drawer.tsx index 6ad48c131..5ed075046 100644 --- a/ui/src/components/task/task-drawer.tsx +++ b/ui/src/components/task/task-drawer.tsx @@ -27,9 +27,9 @@ const TaskDrawer = () => { const { data: count, error: countError, + isLoading: isCountLoading, mutate: mutateCount, } = TaskAPI.useGetCount(swrConfig()) - const isCountLoading = !count && !countError const isCountError = !count && countError const isCountReady = count && !countError diff --git a/ui/src/components/task/task-list.tsx b/ui/src/components/task/task-list.tsx index e382c19ef..1c5966a19 100644 --- a/ui/src/components/task/task-list.tsx +++ b/ui/src/components/task/task-list.tsx @@ -27,9 +27,9 @@ const TasksList = () => { const { data: list, error: listError, + isLoading: isListLoading, mutate: mutateList, } = TaskAPI.useList({ page, size: 5, sortOrder: SortOrder.Asc }, swrConfig()) - const isListLoading = !list && !listError const isListError = !list && listError const isListEmpty = list && !listError && list.totalElements === 0 const isListReady = list && !listError && list.totalElements > 0 diff --git a/ui/src/components/workspace/workspace-edit-storage-capacity.tsx b/ui/src/components/workspace/workspace-edit-storage-capacity.tsx index d03790c89..ee724144b 100644 --- a/ui/src/components/workspace/workspace-edit-storage-capacity.tsx +++ b/ui/src/components/workspace/workspace-edit-storage-capacity.tsx @@ -55,8 +55,11 @@ const WorkspaceEditStorageCapacity = ({ }: WorkspaceEditStorageCapacityProps) => { const mutate = useAppSelector((state) => state.ui.workspace.mutate) const [isModalOpen, setIsModalOpen] = useState(false) - const { data: storageUsage, error: storageUsageError } = - StorageAPI.useGetWorkspaceUsage(workspace.id, swrConfig()) + const { + data: storageUsage, + error: storageUsageError, + isLoading: isStorageUsageLoading, + } = StorageAPI.useGetWorkspaceUsage(workspace.id, swrConfig()) const formSchema = useMemo(() => { if (storageUsage) { return Yup.object().shape({ @@ -69,7 +72,6 @@ const WorkspaceEditStorageCapacity = ({ return null } }, [storageUsage]) - const isStorageUsageLoading = !storageUsage && !storageUsageError const isStorageUsageError = !storageUsage && storageUsageError const isStorageUsageReady = storageUsage && !storageUsageError diff --git a/ui/src/pages/account/account-invitations-page.tsx b/ui/src/pages/account/account-invitations-page.tsx index 21bd6bede..28507a573 100644 --- a/ui/src/pages/account/account-invitations-page.tsx +++ b/ui/src/pages/account/account-invitations-page.tsx @@ -37,7 +37,11 @@ const AccountInvitationsPage = () => { const location = useLocation() const dispatch = useAppDispatch() const toast = useToast() - const { data: user, error: userError } = UserAPI.useGet() + const { + data: user, + error: userError, + isLoading: isUserLoading, + } = UserAPI.useGet() const { page, size, steps, setPage, setSize } = usePagePagination({ navigateFn: navigate, searchFn: () => location.search, @@ -46,15 +50,14 @@ const AccountInvitationsPage = () => { const { data: list, error: listError, + isLoading: isListLoading, mutate, } = InvitationAPI.useGetIncoming( { page, size, sortBy: SortBy.DateCreated, sortOrder: SortOrder.Desc }, swrConfig(), ) - const isUserLoading = !user && !userError const isUserError = !user && userError const isUserReady = user && !userError - const isListLoading = !list && !listError const isListError = !list && listError const isListEmpty = list && !listError && list.totalElements === 0 const isListReady = list && !listError && list.totalElements > 0 diff --git a/ui/src/pages/account/account-layout.tsx b/ui/src/pages/account/account-layout.tsx index fad8e2c97..5be727bb9 100644 --- a/ui/src/pages/account/account-layout.tsx +++ b/ui/src/pages/account/account-layout.tsx @@ -35,11 +35,15 @@ const AccountLayout = () => { const navigate = useNavigate() const dispatch = useAppDispatch() const [isImageModalOpen, setIsImageModalOpen] = useState(false) - const { data: user, error: userError, mutate } = UserAPI.useGet(swrConfig()) + const { + data: user, + isLoading: isUserLoading, + error: userError, + mutate, + } = UserAPI.useGet(swrConfig()) const { data: invitationCount } = InvitationAPI.useGetIncomingCount(swrConfig()) const [tabIndex, setTabIndex] = useState(0) - const isUserLoading = !user && !userError const isUserError = !user && userError const isUserReady = user && !userError diff --git a/ui/src/pages/account/account-settings-page.tsx b/ui/src/pages/account/account-settings-page.tsx index 63d5a06dd..296a20353 100644 --- a/ui/src/pages/account/account-settings-page.tsx +++ b/ui/src/pages/account/account-settings-page.tsx @@ -46,17 +46,22 @@ const EditButton = (props: IconButtonProps) => ( const AccountSettingsPage = () => { const { colorMode, toggleColorMode } = useColorMode() - const { data: user, error: userError } = UserAPI.useGet() - const { data: storageUsage, error: storageUsageError } = - StorageAPI.useGetAccountUsage(swrConfig()) + const { + data: user, + error: userError, + isLoading: isUserLoading, + } = UserAPI.useGet() + const { + data: storageUsage, + error: storageUsageError, + isLoading: isStorageUsageLoading, + } = StorageAPI.useGetAccountUsage(swrConfig()) const [isFullNameModalOpen, setIsFullNameModalOpen] = useState(false) const [isEmailModalOpen, setIsEmailModalOpen] = useState(false) const [isPasswordModalOpen, setIsPasswordModalOpen] = useState(false) const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false) - const isUserLoading = !user && !userError const isUserError = !user && userError const isUserReady = user && !userError - const isStorageUsageLoading = !storageUsage && !storageUsageError const isStorageUsageError = !storageUsage && storageUsageError const isStorageUsageReady = storageUsage && !storageUsageError diff --git a/ui/src/pages/console/console-panel-groups.tsx b/ui/src/pages/console/console-panel-groups.tsx index fe4e7f975..a11b66c57 100644 --- a/ui/src/pages/console/console-panel-groups.tsx +++ b/ui/src/pages/console/console-panel-groups.tsx @@ -51,13 +51,13 @@ const ConsolePanelGroups = () => { const { data: list, error: listError, + isLoading: isListLoading, mutate, } = ConsoleAPI.useListOrSearchObject( 'group', { page, size, query }, swrConfig(), ) - const isListLoading = !list && !listError const isListError = !list && listError const isListEmpty = list && !listError && list.totalElements === 0 const isListReady = list && !listError && list.totalElements > 0 diff --git a/ui/src/pages/console/console-panel-invitations.tsx b/ui/src/pages/console/console-panel-invitations.tsx index 21b2a7145..e56e5e87b 100644 --- a/ui/src/pages/console/console-panel-invitations.tsx +++ b/ui/src/pages/console/console-panel-invitations.tsx @@ -48,13 +48,13 @@ const ConsolePanelInvitations = () => { const { data: list, error: listError, + isLoading: isListLoading, mutate, } = ConsoleAPI.useListObject( 'invitation', { page, size }, swrConfig(), ) - const isListLoading = !list && !listError const isListError = !list && listError const isListEmpty = list && !listError && list.totalElements === 0 const isListReady = list && !listError && list.totalElements > 0 diff --git a/ui/src/pages/console/console-panel-organization.tsx b/ui/src/pages/console/console-panel-organization.tsx index 532407f73..0ee667ac8 100644 --- a/ui/src/pages/console/console-panel-organization.tsx +++ b/ui/src/pages/console/console-panel-organization.tsx @@ -41,50 +41,59 @@ const ConsolePanelOrganization = () => { const [userPage, setUserPage] = useState(1) const [workspacePage, setWorkspacePage] = useState(1) const [groupPage, setGroupPage] = useState(1) - const { data: org, error: orgError } = ConsoleAPI.useGetOrganizationById({ + const { + data: org, + error: orgError, + isLoading: isOrgLoading, + } = ConsoleAPI.useGetOrganizationById({ id, }) - const { data: userList, error: userError } = - ConsoleAPI.useListUsersByOrganization( - { - id, - page: userPage, - size: 5, - }, - swrConfig(), - ) - const { data: workspaceList, error: workspaceError } = - ConsoleAPI.useListWorkspacesByOrganization( - { - id, - page: workspacePage, - size: 5, - }, - swrConfig(), - ) - const { data: groupList, error: groupError } = - ConsoleAPI.useListGroupsByOrganization( - { - id, - page: groupPage, - size: 5, - }, - swrConfig(), - ) - const isOrgLoading = !org && !orgError + const { + data: userList, + error: userError, + isLoading: isUserListLoading, + } = ConsoleAPI.useListUsersByOrganization( + { + id, + page: userPage, + size: 5, + }, + swrConfig(), + ) + const { + data: workspaceList, + isLoading: isWorkspaceListLoading, + error: workspaceError, + } = ConsoleAPI.useListWorkspacesByOrganization( + { + id, + page: workspacePage, + size: 5, + }, + swrConfig(), + ) + const { + data: groupList, + error: groupError, + isLoading: isGroupListLoading, + } = ConsoleAPI.useListGroupsByOrganization( + { + id, + page: groupPage, + size: 5, + }, + swrConfig(), + ) const isOrgError = !org && orgError const isOrgReady = org && !orgError - const isUserListLoading = !userList && !userError const isUserListError = !userList && userError const isUserListEmpty = userList && !userError && userList.totalElements === 0 const isUserListReady = userList && !userError && userList.totalElements > 0 - const isWorkspaceListLoading = !workspaceList && !workspaceError const isWorkspaceListError = !workspaceList && workspaceError const isWorkspaceListEmpty = workspaceList && !workspaceError && workspaceList.totalElements === 0 const isWorkspaceListReady = workspaceList && !workspaceError && workspaceList.totalElements > 0 - const isGroupListLoading = !groupList && !groupError const isGroupListError = !groupList && groupError const isGroupListEmpty = groupList && !groupError && groupList.totalElements === 0 diff --git a/ui/src/pages/console/console-panel-organizations.tsx b/ui/src/pages/console/console-panel-organizations.tsx index 3f15908d4..96e05f0dc 100644 --- a/ui/src/pages/console/console-panel-organizations.tsx +++ b/ui/src/pages/console/console-panel-organizations.tsx @@ -50,13 +50,13 @@ const ConsolePanelOrganizations = () => { const { data: list, error: listError, + isLoading: isListLoading, mutate, } = ConsoleAPI.useListOrSearchObject( 'organization', { page, size, query }, swrConfig(), ) - const isListLoading = !list && !listError const isListError = !list && listError const isListEmpty = list && !listError && list.totalElements === 0 const isListReady = list && !listError && list.totalElements > 0 diff --git a/ui/src/pages/console/console-panel-user.tsx b/ui/src/pages/console/console-panel-user.tsx index 9f62bbe11..3c4a178b9 100644 --- a/ui/src/pages/console/console-panel-user.tsx +++ b/ui/src/pages/console/console-panel-user.tsx @@ -60,8 +60,16 @@ const ConsolePanelUser = () => { const [workspacePage, setWorkspacePage] = useState(1) const [groupPage, setGroupPage] = useState(1) const [orgsPage, setOrgPage] = useState(1) - const { data: user, error: userError } = UserAPI.useGetById(id) - const { data: groupList, error: groupError } = ConsoleAPI.useListGroupsByUser( + const { + data: user, + error: userError, + isLoading: isUserLoading, + } = UserAPI.useGetById(id) + const { + data: groupList, + error: groupError, + isLoading: isGroupListLoading, + } = ConsoleAPI.useListGroupsByUser( { id, page: groupPage, @@ -69,38 +77,40 @@ const ConsolePanelUser = () => { }, swrConfig(), ) - const { data: orgList, error: orgError } = - ConsoleAPI.useListOrganizationsByUser( - { - id, - page: orgsPage, - size: 5, - }, - swrConfig(), - ) - const { data: workspaceList, error: workspaceError } = - ConsoleAPI.useListWorkspacesByUser( - { - id, - page: workspacePage, - size: 5, - }, - swrConfig(), - ) - const isUserLoading = !user && !userError + const { + data: orgList, + error: orgError, + isLoading: isOrgListLoading, + } = ConsoleAPI.useListOrganizationsByUser( + { + id, + page: orgsPage, + size: 5, + }, + swrConfig(), + ) + const { + data: workspaceList, + error: workspaceError, + isLoading: isWorkspaceListLoading, + } = ConsoleAPI.useListWorkspacesByUser( + { + id, + page: workspacePage, + size: 5, + }, + swrConfig(), + ) const isUserError = !user && userError const isUserReady = user && !userError - const isGroupListLoading = !groupList && !groupError const isGroupListError = !groupList && groupError const isGroupListEmpty = groupList && !groupError && groupList.totalElements === 0 const isGroupListReady = groupList && !groupError && groupList.totalElements > 0 - const isOrgListLoading = !orgList && !orgError const isOrgListError = !orgList && orgError const isOrgListEmpty = orgList && !orgError && orgList.totalElements === 0 const isOrgListReady = orgList && !orgError && orgList.totalElements > 0 - const isWorkspaceListLoading = !workspaceList && !workspaceError const isWorkspaceListError = !workspaceList && workspaceError const isWorkspaceListEmpty = workspaceList && !workspaceError && workspaceList.totalElements === 0 diff --git a/ui/src/pages/console/console-panel-users.tsx b/ui/src/pages/console/console-panel-users.tsx index 8778fff66..769d00503 100644 --- a/ui/src/pages/console/console-panel-users.tsx +++ b/ui/src/pages/console/console-panel-users.tsx @@ -62,9 +62,9 @@ const ConsolePanelUsers = () => { const { data: list, error: listError, + isLoading: isListLoading, mutate, } = UserAPI.useList({ query, page, size }, swrConfig()) - const isListLoading = !list && !listError const isListError = !list && listError const isListEmpty = list && !listError && list.totalElements === 0 const isListReady = list && !listError && list.totalElements > 0 diff --git a/ui/src/pages/console/console-panel-workspaces.tsx b/ui/src/pages/console/console-panel-workspaces.tsx index bd29db793..90c27500d 100644 --- a/ui/src/pages/console/console-panel-workspaces.tsx +++ b/ui/src/pages/console/console-panel-workspaces.tsx @@ -52,13 +52,13 @@ const ConsolePanelWorkspaces = () => { const { data: list, error: listError, + isLoading: isListLoading, mutate, } = ConsoleAPI.useListOrSearchObject( 'workspace', { page, size, query }, swrConfig(), ) - const isListLoading = !list && !listError const isListError = !list && listError const isListEmpty = list && !listError && list.totalElements === 0 const isListReady = list && !listError && list.totalElements > 0 diff --git a/ui/src/pages/group/group-layout.tsx b/ui/src/pages/group/group-layout.tsx index fd3838400..0709c3dcf 100644 --- a/ui/src/pages/group/group-layout.tsx +++ b/ui/src/pages/group/group-layout.tsx @@ -26,10 +26,10 @@ const GroupLayout = () => { const { data: group, error: groupError, + isLoading: isGroupLoading, mutate, } = GroupAPI.useGet(id, swrConfig()) const [tabIndex, setTabIndex] = useState(0) - const isGroupLoading = !group && !groupError const isGroupError = !group && groupError const isGroupReady = group && !groupError diff --git a/ui/src/pages/group/group-list-page.tsx b/ui/src/pages/group/group-list-page.tsx index 7db0b3684..940316bf2 100644 --- a/ui/src/pages/group/group-list-page.tsx +++ b/ui/src/pages/group/group-list-page.tsx @@ -49,12 +49,12 @@ const GroupListPage = () => { const { data: list, error: listError, + isLoading: isListLoading, mutate, } = GroupAPI.useList( { query, page, size, sortOrder: SortOrder.Desc }, swrConfig(), ) - const isListLoading = !list && !listError const isListError = !list && listError const isListEmpty = list && !listError && list.totalElements === 0 const isListReady = list && !listError && list.totalElements > 0 diff --git a/ui/src/pages/group/group-members-page.tsx b/ui/src/pages/group/group-members-page.tsx index 5f0e760c4..1233be4bf 100644 --- a/ui/src/pages/group/group-members-page.tsx +++ b/ui/src/pages/group/group-members-page.tsx @@ -47,7 +47,11 @@ const GroupMembersPage = () => { const dispatch = useAppDispatch() const location = useLocation() const { id } = useParams() - const { data: group, error: groupError } = GroupAPI.useGet(id, swrConfig()) + const { + data: group, + error: groupError, + isLoading: isGroupLoading, + } = GroupAPI.useGet(id, swrConfig()) const { page, size, steps, setPage, setSize } = usePagePagination({ navigateFn: navigate, searchFn: () => location.search, @@ -58,6 +62,7 @@ const GroupMembersPage = () => { const { data: list, error: listError, + isLoading: isListLoading, mutate, } = UserAPI.useList( { @@ -74,10 +79,8 @@ const GroupMembersPage = () => { const [isAddMembersModalOpen, setIsAddMembersModalOpen] = useState(false) const [isRemoveMemberModalOpen, setIsRemoveMemberModalOpen] = useState(false) - const isGroupLoading = !group && !groupError const isGroupError = !group && groupError const isGroupReady = group && !groupError - const isListLoading = !list && !listError const isListError = !list && listError const isListEmpty = list && !listError && list.totalElements === 0 const isListReady = list && !listError && list.totalElements > 0 diff --git a/ui/src/pages/group/group-settings-page.tsx b/ui/src/pages/group/group-settings-page.tsx index f8233c716..a309dc945 100644 --- a/ui/src/pages/group/group-settings-page.tsx +++ b/ui/src/pages/group/group-settings-page.tsx @@ -28,7 +28,11 @@ import { truncateEnd } from '@/lib/helpers/truncate-end' const GroupSettingsPage = () => { const { id } = useParams() - const { data: group, error: groupError } = GroupAPI.useGet(id, swrConfig()) + const { + data: group, + error: groupError, + isLoading: isGroupLoading, + } = GroupAPI.useGet(id, swrConfig()) const [isNameModalOpen, setIsNameModalOpen] = useState(false) const [isAddMembersModalOpen, setIsAddMembersModalOpen] = useState(false) const [deleteModalOpen, setDeleteModalOpen] = useState(false) @@ -40,7 +44,6 @@ const GroupSettingsPage = () => { () => group && geOwnerPermission(group.permission), [group], ) - const isGroupLoading = !group && !groupError const isGroupError = !group && groupError const isGroupReady = group && !groupError diff --git a/ui/src/pages/organization/organization-invitations-page.tsx b/ui/src/pages/organization/organization-invitations-page.tsx index 53d7d9c89..cb20b8a3a 100644 --- a/ui/src/pages/organization/organization-invitations-page.tsx +++ b/ui/src/pages/organization/organization-invitations-page.tsx @@ -40,7 +40,11 @@ const OrganizationInvitationsPage = () => { const location = useLocation() const { id } = useParams() const toast = useToast() - const { data: org, error: orgError } = OrganizationAPI.useGet(id, swrConfig()) + const { + data: org, + error: orgError, + isLoading: isOrgLoading, + } = OrganizationAPI.useGet(id, swrConfig()) const { page, size, steps, setPage, setSize } = usePagePagination({ navigateFn: navigate, searchFn: () => location.search, @@ -49,6 +53,7 @@ const OrganizationInvitationsPage = () => { const { data: list, error: listError, + isLoading: isListLoading, mutate, } = InvitationAPI.useGetOutgoing( { @@ -62,10 +67,8 @@ const OrganizationInvitationsPage = () => { ) const [isInviteMembersModalOpen, setIsInviteMembersModalOpen] = useState(false) - const isOrgLoading = !org && !orgError const isOrgError = !org && orgError const isOrgReady = org && !orgError - const isListLoading = !list && !listError const isListError = !list && listError const isListEmpty = list && !listError && list.totalElements === 0 const isListReady = list && !listError && list.totalElements > 0 diff --git a/ui/src/pages/organization/organization-layout.tsx b/ui/src/pages/organization/organization-layout.tsx index 457663c8e..344957d05 100644 --- a/ui/src/pages/organization/organization-layout.tsx +++ b/ui/src/pages/organization/organization-layout.tsx @@ -27,10 +27,10 @@ const OrganizationLayout = () => { const { data: org, error: orgError, + isLoading: isOrgLoading, mutate, } = OrganizationAPI.useGet(id, swrConfig()) const [tabIndex, setTabIndex] = useState(0) - const isOrgLoading = !org && !orgError const isOrgError = !org && orgError const isOrgReady = org && !orgError diff --git a/ui/src/pages/organization/organization-list-page.tsx b/ui/src/pages/organization/organization-list-page.tsx index 985098926..9bf092443 100644 --- a/ui/src/pages/organization/organization-list-page.tsx +++ b/ui/src/pages/organization/organization-list-page.tsx @@ -49,12 +49,12 @@ const OrganizationListPage = () => { const { data: list, error: listError, + isLoading: isListLoading, mutate, } = OrganizationAPI.useList( { query, page, size, sortOrder: SortOrder.Desc }, swrConfig(), ) - const isListLoading = !list && !listError const isListError = !list && listError const isListEmpty = list && !listError && list.totalElements === 0 const isListReady = list && !listError && list.totalElements > 0 diff --git a/ui/src/pages/organization/organization-members-page.tsx b/ui/src/pages/organization/organization-members-page.tsx index bf8904712..e88c5f8ab 100644 --- a/ui/src/pages/organization/organization-members-page.tsx +++ b/ui/src/pages/organization/organization-members-page.tsx @@ -49,7 +49,11 @@ const OrganizationMembersPage = () => { const location = useLocation() const dispatch = useAppDispatch() const { id } = useParams() - const { data: org, error: orgError } = OrganizationAPI.useGet(id, swrConfig()) + const { + data: org, + error: orgError, + isLoading: isOrgLoading, + } = OrganizationAPI.useGet(id, swrConfig()) const { page, size, steps, setPage, setSize } = usePagePagination({ navigateFn: navigate, searchFn: () => location.search, @@ -60,6 +64,7 @@ const OrganizationMembersPage = () => { const { data: list, error: listError, + isLoading: isListLoading, mutate, } = UserAPI.useList( { @@ -78,10 +83,8 @@ const OrganizationMembersPage = () => { const [userToRemove, setUserToRemove] = useState() const [isRemoveMemberModalOpen, setIsRemoveMemberModalOpen] = useState(false) - const isOrgLoading = !org && !orgError const isOrgError = !org && orgError const isOrgReady = org && !orgError - const isListLoading = !list && !listError const isListError = !list && listError const isListEmpty = list && !listError && list.totalElements === 0 const isListReady = list && !listError && list.totalElements > 0 diff --git a/ui/src/pages/organization/organization-settings-page.tsx b/ui/src/pages/organization/organization-settings-page.tsx index ba86ed7ab..e262b1c2d 100644 --- a/ui/src/pages/organization/organization-settings-page.tsx +++ b/ui/src/pages/organization/organization-settings-page.tsx @@ -30,13 +30,16 @@ import { truncateEnd } from '@/lib/helpers/truncate-end' const OrganizationSettingsPage = () => { const { id } = useParams() - const { data: org, error: orgError } = OrganizationAPI.useGet(id, swrConfig()) + const { + data: org, + error: orgError, + isLoading: isOrgLoading, + } = OrganizationAPI.useGet(id, swrConfig()) const [isNameModalOpen, setIsNameModalOpen] = useState(false) const [isInviteMembersModalOpen, setIsInviteMembersModalOpen] = useState(false) const [isLeaveModalOpen, setIsLeaveModalOpen] = useState(false) const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false) - const isOrgLoading = !org && !orgError const isOrgError = !org && orgError const isOrgReady = org && !orgError From b9ca7cb0835b70a5658ce753ea214954343a41a8 Mon Sep 17 00:00:00 2001 From: Anass Bouassaba Date: Wed, 13 Nov 2024 20:46:09 +0100 Subject: [PATCH 11/13] refactor(ui): remove unused import --- ui/src/components/file/list/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/components/file/list/index.tsx b/ui/src/components/file/list/index.tsx index caf120391..960012540 100644 --- a/ui/src/components/file/list/index.tsx +++ b/ui/src/components/file/list/index.tsx @@ -9,7 +9,7 @@ // licenses/AGPL.txt. import { MouseEvent, useCallback, useEffect, useMemo, useState } from 'react' import { useParams } from 'react-router-dom' -import { SectionPlaceholder, SectionSpinner } from '@koupr/ui' +import { SectionPlaceholder } from '@koupr/ui' import { DndContext, useSensors, From df339ac87f3da9f1a7dec56a8cf6a53c11af24ef Mon Sep 17 00:00:00 2001 From: Anass Bouassaba Date: Wed, 13 Nov 2024 20:47:47 +0100 Subject: [PATCH 12/13] fix(ui): remove troublesome return null --- ui/src/pages/workspace/workspace-layout.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ui/src/pages/workspace/workspace-layout.tsx b/ui/src/pages/workspace/workspace-layout.tsx index 0d76c0e26..7a9c224fc 100644 --- a/ui/src/pages/workspace/workspace-layout.tsx +++ b/ui/src/pages/workspace/workspace-layout.tsx @@ -49,10 +49,6 @@ const WorkspaceLayout = () => { } }, [mutate, dispatch]) - if (!workspace) { - return null - } - return ( <> {isWorkspaceLoading ? : null} From 4063f9be8c8e560f1bb24e6dcc7abfbe88db2d18 Mon Sep 17 00:00:00 2001 From: Anass Bouassaba Date: Wed, 13 Nov 2024 20:49:34 +0100 Subject: [PATCH 13/13] fix(ui): remove troublesome return null --- ui/src/pages/organization/organization-layout.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ui/src/pages/organization/organization-layout.tsx b/ui/src/pages/organization/organization-layout.tsx index 344957d05..45fc832af 100644 --- a/ui/src/pages/organization/organization-layout.tsx +++ b/ui/src/pages/organization/organization-layout.tsx @@ -52,10 +52,6 @@ const OrganizationLayout = () => { } }, [mutate, dispatch]) - if (!org) { - return null - } - return ( <> {isOrgLoading ? : null}