This is an automated email from the ASF dual-hosted git repository. porcelli pushed a commit to branch KOGITO-8015-feature-preview in repository https://gitbox.apache.org/repos/asf/incubator-kie-tools-temporary-rnd-do-not-use.git
commit 1ec224401674968dfc4495866695614b8554eafb Author: Fabrizio Antonangeli <[email protected]> AuthorDate: Fri May 5 13:14:34 2023 +0200 KOGITO-8890: Refactor workspace files listing according to UX redesign (#1606) Co-authored-by: Guilherme Caponetto <[email protected]> --- .../serverless-logic-web-tools/src/AppConstants.ts | 2 + .../src/editor/EditorToolbar.tsx | 4 +- .../src/homepage/overView/Overview.tsx | 3 +- .../src/homepage/pageTemplate/OnlineEditorPage.tsx | 27 +- .../homepage/recentModels/ConfirmDeleteModal.tsx | 154 ----------- .../src/homepage/recentModels/RecentModels.tsx | 151 ++++++---- .../src/homepage/recentModels/WorkspacesTable.tsx | 7 +- .../homepage/recentModels/WorkspacesTableRow.tsx | 17 +- .../recentModels/workspaceFiles/WorkspaceFiles.tsx | 304 +++++++++++++++++++++ .../workspaceFiles/WorkspaceFilesTable.tsx | 159 +++++++++++ .../workspaceFiles/WorkspaceFilesTableRow.tsx | 100 +++++++ .../src/homepage/routes/HomePageRoutes.tsx | 4 + .../src/homepage/uiNav/HomePageNav.tsx | 19 +- .../src/navigation/Routes.ts | 4 + .../src/table/ConfirmDeleteModal.tsx | 96 +++++++ .../src/table/TablePagination.tsx | 12 +- .../src/table/TableToolbar.tsx | 3 + .../static/resources/style.css | 5 + 18 files changed, 825 insertions(+), 246 deletions(-) diff --git a/packages/serverless-logic-web-tools/src/AppConstants.ts b/packages/serverless-logic-web-tools/src/AppConstants.ts index 4c8316aa70..8124e259f5 100644 --- a/packages/serverless-logic-web-tools/src/AppConstants.ts +++ b/packages/serverless-logic-web-tools/src/AppConstants.ts @@ -15,3 +15,5 @@ */ export const APP_NAME = "Serverless Logic Web Tools"; +export const SERVERLESS_LOGIC_WEBTOOLS_DOCUMENTATION_URL = + "https://kiegroup.github.io/kogito-docs/serverlessworkflow/latest/tooling/serverless-logic-web-tools/serverless-logic-web-tools-overview.html"; diff --git a/packages/serverless-logic-web-tools/src/editor/EditorToolbar.tsx b/packages/serverless-logic-web-tools/src/editor/EditorToolbar.tsx index 27d5cdebb4..50a4dc4635 100644 --- a/packages/serverless-logic-web-tools/src/editor/EditorToolbar.tsx +++ b/packages/serverless-logic-web-tools/src/editor/EditorToolbar.tsx @@ -1313,7 +1313,7 @@ If you are, it means that creating this Gist failed and it can safely be deleted <Button className={"kie-tools--masthead-hoverable"} variant={ButtonVariant.plain} - onClick={() => history.push({ pathname: routes.recentModels.path({}) })} + onClick={() => history.push({ pathname: routes.workspaceWithFiles.path(props.workspaceFile) })} > <AngleLeftIcon /> </Button> @@ -1512,7 +1512,7 @@ If you are, it means that creating this Gist failed and it can safely be deleted toggle={ <DropdownToggle onToggle={setNewFileDropdownMenuOpen} - isPrimary={true} + toggleVariant="primary" toggleIndicator={CaretDownIcon} > <PlusIcon /> diff --git a/packages/serverless-logic-web-tools/src/homepage/overView/Overview.tsx b/packages/serverless-logic-web-tools/src/homepage/overView/Overview.tsx index b49ecb1f3a..ac72570919 100644 --- a/packages/serverless-logic-web-tools/src/homepage/overView/Overview.tsx +++ b/packages/serverless-logic-web-tools/src/homepage/overView/Overview.tsx @@ -36,6 +36,7 @@ import { CardHeader, CardHeaderMain, CardTitle } from "@patternfly/react-core/di import { List, ListItem } from "@patternfly/react-core/dist/js/components/List"; import { QuickStartContext, QuickStartContextValues } from "@patternfly/quickstarts"; import { ExternalLinkAltIcon } from "@patternfly/react-icons/dist/js/icons"; +import { SERVERLESS_LOGIC_WEBTOOLS_DOCUMENTATION_URL } from "../../AppConstants"; export function Overview(props: { isNavOpen: boolean }) { const routes = useRoutes(); @@ -106,7 +107,7 @@ export function Overview(props: { isNavOpen: boolean }) { target="_blank" iconPosition="right" icon={<ExternalLinkAltIcon />} - href="https://kiegroup.github.io/kogito-docs/serverlessworkflow/latest/index.html" + href={SERVERLESS_LOGIC_WEBTOOLS_DOCUMENTATION_URL} variant={ButtonVariant.secondary} component="a" > diff --git a/packages/serverless-logic-web-tools/src/homepage/pageTemplate/OnlineEditorPage.tsx b/packages/serverless-logic-web-tools/src/homepage/pageTemplate/OnlineEditorPage.tsx index 7d481676b1..e37a20feb0 100644 --- a/packages/serverless-logic-web-tools/src/homepage/pageTemplate/OnlineEditorPage.tsx +++ b/packages/serverless-logic-web-tools/src/homepage/pageTemplate/OnlineEditorPage.tsx @@ -17,7 +17,6 @@ import * as React from "react"; import { QuickStartContainer, QuickStartContainerProps } from "@patternfly/quickstarts"; import { Brand } from "@patternfly/react-core/dist/js/components/Brand"; -import { Button } from "@patternfly/react-core/dist/js/components/Button"; import { Masthead, MastheadBrand, @@ -28,7 +27,7 @@ import { import { PageSidebar } from "@patternfly/react-core/dist/js/components/Page/PageSidebar"; import { SkipToContent } from "@patternfly/react-core/dist/js/components/SkipToContent"; import { Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem } from "@patternfly/react-core/dist/js/components/Toolbar"; -import { Page } from "@patternfly/react-core/dist/js/components/Page"; +import { Page, PageToggleButton } from "@patternfly/react-core/dist/js/components/Page"; import { Tooltip } from "@patternfly/react-core/dist/js/components/Tooltip"; import { BarsIcon, ExclamationIcon } from "@patternfly/react-icons/dist/js/icons"; import { useMemo, useState } from "react"; @@ -45,6 +44,7 @@ import { } from "../../quickstarts-data"; import { SettingsButton } from "../../settings/SettingsButton"; import { HomePageNav } from "../uiNav/HomePageNav"; +import { APP_NAME } from "../../AppConstants"; export type OnlineEditorPageProps = { children?: React.ReactNode; @@ -57,9 +57,6 @@ export function OnlineEditorPage(props: OnlineEditorPageProps) { const history = useHistory(); const routes = useRoutes(); const isRouteInSettingsSection = useRouteMatch(routes.settings.home.path({})); - const navToggle = () => { - props.setIsNavOpen(!props.isNavOpen); - }; const [activeQuickStartID, setActiveQuickStartID] = useState(""); const [allQuickStartStates, setAllQuickStartStates] = useState({}); @@ -120,27 +117,17 @@ export function OnlineEditorPage(props: OnlineEditorPageProps) { const masthead = ( <Masthead> <MastheadToggle> - <Button - id="nav-toggle" - variant="plain" - aria-label="Global NAV" - onClick={navToggle} - aria-expanded={props.isNavOpen} - aria-controls="" - > + <PageToggleButton variant="plain" aria-label="Global NAV"> <BarsIcon /> - </Button> + </PageToggleButton> </MastheadToggle> <MastheadMain> <MastheadBrand onClick={() => history.push({ pathname: routes.home.path({}) })} style={{ textDecoration: "none" }} > - <Brand - className="kogito-tools-common--brand" - src="images/kogito_log_workbranch.svg" - alt="kogito_logo_white.png" - ></Brand> + <Brand className="kogito-tools-common--brand" src="favicon.svg" alt="Kie logo"></Brand> + <div className="brand-name">{APP_NAME}</div> </MastheadBrand> </MastheadMain> <MastheadContent>{headerToolbar}</MastheadContent> @@ -158,7 +145,7 @@ export function OnlineEditorPage(props: OnlineEditorPageProps) { [location, isRouteInSettingsSection] ); - const sidebar = <PageSidebar nav={pageNav} isNavOpen={props.isNavOpen} theme="dark" />; + const sidebar = <PageSidebar nav={pageNav} theme="dark" />; const mainContainerId = "main-content-page-layout-tertiary-nav"; const pageSkipToContent = <SkipToContent href={`#${mainContainerId}`}>Skip to content</SkipToContent>; diff --git a/packages/serverless-logic-web-tools/src/homepage/recentModels/ConfirmDeleteModal.tsx b/packages/serverless-logic-web-tools/src/homepage/recentModels/ConfirmDeleteModal.tsx deleted file mode 100644 index 4d260c06af..0000000000 --- a/packages/serverless-logic-web-tools/src/homepage/recentModels/ConfirmDeleteModal.tsx +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright 2023 Red Hat, Inc. and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import * as React from "react"; -import { WorkspaceDescriptor } from "@kie-tools-core/workspaces-git-fs/dist/worker/api/WorkspaceDescriptor"; -import { Button, Checkbox, Modal, ModalProps, Skeleton } from "@patternfly/react-core/dist/js"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { useWorkspaces } from "@kie-tools-core/workspaces-git-fs/dist/context/WorkspacesContext"; -import { splitFiles } from "../../extension"; - -export function ConfirmDeleteModal( - props: { - onDelete: () => void; - selectedWorkspaceIds: WorkspaceDescriptor["workspaceId"][]; - } & Pick<ModalProps, "isOpen" | "onClose"> -) { - const { selectedWorkspaceIds, isOpen, onClose, onDelete } = props; - const [isDeleteCheck, setIsDeleteCheck] = useState(false); - const [firstSelectedWorkspaceName, setFirstSelectedWorkspaceName] = useState(""); - const [selectedFoldersCount, setSelectedFoldersCount] = useState(0); - const [elementsTypeName, setElementsTypeName] = useState("models"); - const [dataLoaded, setDataLoaded] = useState(false); - const [fetchError, setFetchError] = useState(false); - const workspaces = useWorkspaces(); - - const isPlural = useMemo(() => selectedWorkspaceIds.length > 1, [selectedWorkspaceIds]); - - const isWsFolder = useCallback( - async (workspaceId: WorkspaceDescriptor["workspaceId"]) => { - const { editableFiles, readonlyFiles } = splitFiles(await workspaces.getFiles({ workspaceId })); - return editableFiles.length > 1 || readonlyFiles.length > 0; - }, - [workspaces] - ); - - const getWorkspaceName = useCallback( - async (workspaceId: WorkspaceDescriptor["workspaceId"]) => { - if (selectedWorkspaceIds.length !== 1) { - return ""; - } - const workspaceData = await workspaces.getWorkspace({ workspaceId }); - return (await isWsFolder(workspaceId)) - ? workspaceData.name - : (await workspaces.getFiles({ workspaceId }))[0].nameWithoutExtension; - }, - [isWsFolder, selectedWorkspaceIds, workspaces] - ); - - const onDeleteCheckChange = useCallback((checked: boolean) => { - setIsDeleteCheck(checked); - }, []); - - useEffect(() => { - if (!isOpen) { - return; - } - - const allPromises: Promise<void>[] = []; - - setIsDeleteCheck(false); - setDataLoaded(false); - setFetchError(false); - - if (selectedWorkspaceIds.length === 1) { - allPromises.push(getWorkspaceName(selectedWorkspaceIds[0]).then(setFirstSelectedWorkspaceName)); - } - - allPromises.push( - Promise.all(selectedWorkspaceIds.map(isWsFolder)).then((results) => { - const foldersCount = results.filter((r) => r).length; - setSelectedFoldersCount(foldersCount); - if (isPlural) { - setElementsTypeName(foldersCount ? "workspaces" : "models"); - } else { - setElementsTypeName(foldersCount ? "workspace" : "model"); - } - }) - ); - - Promise.all(allPromises) - .then(() => setDataLoaded(true)) - .catch((error) => { - console.error("Error retrieving workspace data:", error); - setFetchError(true); - }); - }, [selectedWorkspaceIds, isWsFolder, isOpen, getWorkspaceName, isPlural]); - - return ( - <> - <Modal - title={`Delete ${elementsTypeName}`} - titleIconVariant={"warning"} - isOpen={isOpen && !fetchError} - onClose={onClose} - aria-describedby="modal-custom-icon-description" - actions={[ - dataLoaded ? ( - <Button key="confirm" variant="danger" onClick={onDelete} isDisabled={!isDeleteCheck} aria-label="Delete"> - Delete {elementsTypeName} - </Button> - ) : ( - <Skeleton width="100px" key="confirm-skeleton" /> - ), - <Button key="cancel" variant="link" onClick={onClose} aria-label="Cancel"> - Cancel - </Button>, - ]} - variant="small" - > - {dataLoaded ? ( - <span id="modal-custom-icon-description"> - Deleting {isPlural ? "these" : "this"}{" "} - <b>{isPlural ? selectedWorkspaceIds.length : firstSelectedWorkspaceName}</b> {elementsTypeName} - {selectedFoldersCount ? ` removes the ${elementsTypeName} and all the models inside.` : "."} - </span> - ) : ( - <Skeleton width="80%" /> - )} - <br /> - <br /> - <Checkbox - label="I understand that this action cannot be undone." - id="delete-model-check" - isChecked={isDeleteCheck} - onChange={onDeleteCheckChange} - aria-label="Confirm checkbox delete model" - /> - </Modal> - - <Modal - title={`Error retrieving data`} - titleIconVariant={"danger"} - isOpen={isOpen && fetchError} - onClose={onClose} - aria-describedby="modal-custom-icon-description" - variant="small" - > - <span id="modal-custom-icon-description">An error occurred while loading the data!</span> - </Modal> - </> - ); -} diff --git a/packages/serverless-logic-web-tools/src/homepage/recentModels/RecentModels.tsx b/packages/serverless-logic-web-tools/src/homepage/recentModels/RecentModels.tsx index 964fce3616..c184cce16c 100644 --- a/packages/serverless-logic-web-tools/src/homepage/recentModels/RecentModels.tsx +++ b/packages/serverless-logic-web-tools/src/homepage/recentModels/RecentModels.tsx @@ -15,12 +15,11 @@ */ import { PromiseStateWrapper } from "@kie-tools-core/react-hooks/dist/PromiseState"; +import { useController } from "@kie-tools-core/react-hooks/dist/useController"; import { useWorkspaces } from "@kie-tools-core/workspaces-git-fs/dist/context/WorkspacesContext"; import { useWorkspaceDescriptorsPromise } from "@kie-tools-core/workspaces-git-fs/dist/hooks/WorkspacesHooks"; import { WorkspaceDescriptor } from "@kie-tools-core/workspaces-git-fs/dist/worker/api/WorkspaceDescriptor"; -import { PerPageOptions } from "@patternfly/react-core/dist/js/components/Pagination"; -import { Alert, AlertActionCloseButton, AlertProps } from "@patternfly/react-core/dist/js/components/Alert"; -import { AlertGroup } from "@patternfly/react-core/dist/js/components/AlertGroup"; +import { Alert, AlertActionCloseButton } from "@patternfly/react-core/dist/js/components/Alert"; import { EmptyState, EmptyStateBody, EmptyStateIcon } from "@patternfly/react-core/dist/js/components/EmptyState"; import { Page, PageSection } from "@patternfly/react-core/dist/js/components/Page"; import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/js/components/Text"; @@ -28,39 +27,76 @@ import { Title } from "@patternfly/react-core/dist/js/components/Title"; import { Bullseye } from "@patternfly/react-core/dist/js/layouts/Bullseye"; import { CubesIcon } from "@patternfly/react-icons/dist/js/icons/cubes-icon"; import * as React from "react"; -import { useCallback, useState } from "react"; -import { ConfirmDeleteModal } from "./ConfirmDeleteModal"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Alerts, AlertsController, useAlert } from "../../alerts/Alerts"; +import { splitFiles } from "../../extension"; +import { ConfirmDeleteModal } from "../../table/ConfirmDeleteModal"; +import { defaultPerPageOptions, TablePagination } from "../../table/TablePagination"; import { TableToolbar } from "../../table/TableToolbar"; import { WorkspacesTable } from "./WorkspacesTable"; -import { TablePagination } from "../../table/TablePagination"; - -const perPageOptions: PerPageOptions[] = [5, 10, 20, 50, 100].map((n) => ({ - title: n.toString(), - value: n, -})); export function RecentModels() { const workspaceDescriptorsPromise = useWorkspaceDescriptorsPromise(); const [selectedWorkspaceIds, setSelectedWorkspaceIds] = useState<WorkspaceDescriptor["workspaceId"][]>([]); const [isConfirmDeleteModalOpen, setIsConfirmDeleteModalOpen] = useState(false); - const [alerts, setAlerts] = useState<Partial<AlertProps>[]>([]); const [searchValue, setSearchValue] = React.useState(""); const [page, setPage] = React.useState(1); const [perPage, setPerPage] = React.useState(5); const workspaces = useWorkspaces(); + const [selectedFoldersCount, setSelectedFoldersCount] = useState(0); + const [firstSelectedWorkspaceName, setFirstSelectedWorkspaceName] = useState(""); + const [deleteModalDataLoaded, setDeleteModalDataLoaded] = useState(false); + const [deleteModalFetchError, setDeleteModalFetchError] = useState(false); + const [alerts, alertsRef] = useController<AlertsController>(); + const isSelectedWorkspacePlural = useMemo(() => selectedWorkspaceIds.length > 1, [selectedWorkspaceIds]); + + const selectedElementTypesName = useMemo(() => { + if (selectedWorkspaceIds.length > 1) { + return selectedFoldersCount ? "workspaces" : "models"; + } + return selectedFoldersCount ? "workspace" : "model"; + }, [selectedFoldersCount, selectedWorkspaceIds]); + + const deleteModalMessage = useMemo( + () => ( + <> + Deleting {isSelectedWorkspacePlural ? "these" : "this"}{" "} + <b>{isSelectedWorkspacePlural ? selectedWorkspaceIds.length : firstSelectedWorkspaceName}</b>{" "} + {selectedElementTypesName} + {selectedFoldersCount ? ` removes the ${selectedElementTypesName} and all the models inside.` : "."} + </> + ), + [ + isSelectedWorkspacePlural, + selectedWorkspaceIds, + firstSelectedWorkspaceName, + selectedElementTypesName, + selectedFoldersCount, + ] + ); const onConfirmDeleteModalClose = useCallback(() => setIsConfirmDeleteModalOpen(false), []); - const addAlert = useCallback( - (title: string, variant: AlertProps["variant"], key: React.Key = new Date().getTime()) => { - setAlerts((prevAlerts) => [...prevAlerts, { title, variant, key }]); - }, - [] + const deleteSuccessAlert = useAlert<{ modelsWord: string }>( + alerts, + useCallback(({ close }, { modelsWord }) => { + return <Alert variant="success" title={`${capitalizeString(modelsWord)} deleted successfully`} />; + }, []), + { durationInSeconds: 2 } ); - const removeAlert = useCallback((key: React.Key) => { - setAlerts((prevAlerts) => [...prevAlerts.filter((alert) => alert.key !== key)]); - }, []); + const deleteErrorAlert = useAlert<{ modelsWord: string }>( + alerts, + useCallback(({ close }, { modelsWord }) => { + return ( + <Alert + variant="danger" + title={`Oops, something went wrong while trying to delete the selected ${modelsWord}. Please refresh the page and try again. If the problem persists, you can try deleting site data for this application in your browser's settings.`} + actionClose={<AlertActionCloseButton onClose={close} />} + /> + ); + }, []) + ); const onConfirmDeleteModalDelete = useCallback( async (workspaceDescriptors: WorkspaceDescriptor[]) => { @@ -73,20 +109,17 @@ export function RecentModels() { .map((w) => workspaces.deleteWorkspace(w)) ) .then(() => { - addAlert(`${modelsWord} deleted successfully`, "success"); + deleteSuccessAlert.show({ modelsWord }); }) .catch((e) => { console.error(e); - addAlert( - `Oops, something went wrong while trying to delete the selected ${modelsWord}. Please refresh the page and try again. If the problem persists, you can try deleting site data for this application in your browser's settings.`, - "danger" - ); + deleteErrorAlert.show({ modelsWord }); }) .finally(() => { setSelectedWorkspaceIds([]); }); }, - [selectedWorkspaceIds, addAlert, workspaces] + [selectedWorkspaceIds, workspaces, deleteErrorAlert, deleteSuccessAlert] ); const onWsToggle = useCallback((workspaceId: WorkspaceDescriptor["workspaceId"], checked: boolean) => { @@ -104,6 +137,39 @@ export function RecentModels() { setSearchValue(""); }, []); + const isWsFolder = useCallback( + async (workspaceId: WorkspaceDescriptor["workspaceId"]) => { + const { editableFiles, readonlyFiles } = splitFiles(await workspaces.getFiles({ workspaceId })); + return editableFiles.length > 1 || readonlyFiles.length > 0; + }, + [workspaces] + ); + + const getWorkspaceName = useCallback( + async (workspaceId: WorkspaceDescriptor["workspaceId"]) => { + if (selectedWorkspaceIds.length !== 1) { + return ""; + } + const workspaceData = await workspaces.getWorkspace({ workspaceId }); + return (await isWsFolder(workspaceId)) + ? workspaceData.name + : (await workspaces.getFiles({ workspaceId }))[0].nameWithoutExtension; + }, + [isWsFolder, selectedWorkspaceIds, workspaces] + ); + + useEffect(() => { + Promise.all([ + Promise.all(selectedWorkspaceIds.map(isWsFolder)).then((results) => { + const foldersCount = results.filter((r) => r).length; + setSelectedFoldersCount(foldersCount); + }), + getWorkspaceName(selectedWorkspaceIds[0]).then(setFirstSelectedWorkspaceName), + ]) + .then(() => setDeleteModalDataLoaded(true)) + .catch(() => setDeleteModalFetchError(true)); + }, [getWorkspaceName, selectedWorkspaceIds, isWsFolder]); + return ( <PromiseStateWrapper promise={workspaceDescriptorsPromise} @@ -113,27 +179,7 @@ export function RecentModels() { return ( <> - <AlertGroup isToast isLiveRegion> - {alerts.map( - ({ key, variant, title }) => - key && ( - <Alert - variant={variant} - title={title} - timeout - onTimeout={() => removeAlert(key)} - actionClose={ - <AlertActionCloseButton - title={title as string} - variantLabel={`${variant} alert`} - onClose={() => removeAlert(key)} - /> - } - key={key} - /> - ) - )} - </AlertGroup> + <Alerts ref={alertsRef} width={"500px"} /> <Page> <PageSection variant={"light"}> <TextContent> @@ -157,7 +203,7 @@ export function RecentModels() { setSearchValue={setSearchValue} page={page} perPage={perPage} - perPageOptions={perPageOptions} + perPageOptions={defaultPerPageOptions} setPage={setPage} setPerPage={setPerPage} /> @@ -174,7 +220,7 @@ export function RecentModels() { itemCount={itemCount} page={page} perPage={perPage} - perPageOptions={perPageOptions} + perPageOptions={defaultPerPageOptions} setPage={setPage} setPerPage={setPerPage} variant="bottom" @@ -196,10 +242,13 @@ export function RecentModels() { </PageSection> </Page> <ConfirmDeleteModal - selectedWorkspaceIds={selectedWorkspaceIds} isOpen={isConfirmDeleteModalOpen} onClose={onConfirmDeleteModalClose} onDelete={() => onConfirmDeleteModalDelete(workspaceDescriptors)} + elementsTypeName={selectedElementTypesName} + deleteMessage={deleteModalMessage} + dataLoaded={deleteModalDataLoaded} + fetchError={deleteModalFetchError} /> </> ); @@ -207,3 +256,5 @@ export function RecentModels() { /> ); } + +const capitalizeString = (value: string) => value.charAt(0).toUpperCase() + value.slice(1); diff --git a/packages/serverless-logic-web-tools/src/homepage/recentModels/WorkspacesTable.tsx b/packages/serverless-logic-web-tools/src/homepage/recentModels/WorkspacesTable.tsx index 9ae35c5f1a..11fde904d4 100644 --- a/packages/serverless-logic-web-tools/src/homepage/recentModels/WorkspacesTable.tsx +++ b/packages/serverless-logic-web-tools/src/homepage/recentModels/WorkspacesTable.tsx @@ -90,11 +90,12 @@ export function WorkspacesTable(props: WorkspacesTableProps) { allWorkspacePromises.status !== PromiseStateStatus.RESOLVED ? [] : workspaceDescriptors.map((workspace, index) => { - const { editableFiles, readonlyFiles } = splitFiles(allWorkspacePromises.data[index] ?? []); + const allFiles = allWorkspacePromises.data[index]; + const { editableFiles, readonlyFiles } = splitFiles(allFiles ?? []); const isWsFolder = editableFiles.length > 1 || readonlyFiles.length > 0 || workspace.origin.kind !== WorkspaceKind.LOCAL; - const hasErrors = !editableFiles || !editableFiles[0]; - const name = getWorkspaceName(workspace, isWsFolder, hasErrors, editableFiles); + const hasErrors = !allFiles[0]; + const name = getWorkspaceName(workspace, isWsFolder, hasErrors, allFiles); return { createdDateISO: workspace.createdDateISO, diff --git a/packages/serverless-logic-web-tools/src/homepage/recentModels/WorkspacesTableRow.tsx b/packages/serverless-logic-web-tools/src/homepage/recentModels/WorkspacesTableRow.tsx index 68eb97634b..84a47ae038 100644 --- a/packages/serverless-logic-web-tools/src/homepage/recentModels/WorkspacesTableRow.tsx +++ b/packages/serverless-logic-web-tools/src/homepage/recentModels/WorkspacesTableRow.tsx @@ -22,7 +22,6 @@ import { EmptyState, EmptyStateBody, EmptyStateIcon } from "@patternfly/react-co import { Popover } from "@patternfly/react-core/dist/js/components/Popover"; import { Title } from "@patternfly/react-core/dist/js/components/Title"; import { Skeleton } from "@patternfly/react-core/dist/js/components/Skeleton"; -import "@patternfly/react-core/dist/styles/base.css"; import { ExclamationTriangleIcon, OutlinedQuestionCircleIcon, SearchIcon } from "@patternfly/react-icons/dist/js/icons"; import { FolderIcon } from "@patternfly/react-icons/dist/js/icons/folder-icon"; import { TaskIcon } from "@patternfly/react-icons/dist/js/icons/task-icon"; @@ -56,12 +55,14 @@ export function WorkspacesTableRow(props: WorkspacesTableRowProps) { const linkTo = useMemo( () => - routes.workspaceWithFilePath.path({ - workspaceId: editableFiles[0].workspaceId, - fileRelativePath: editableFiles[0].relativePathWithoutExtension, - extension: editableFiles[0].extension, - }), - [editableFiles] + !isWsFolder + ? routes.workspaceWithFilePath.path({ + workspaceId, + fileRelativePath: editableFiles[0].relativePathWithoutExtension, + extension: editableFiles[0].extension, + }) + : routes.workspaceWithFiles.path({ workspaceId }), + [editableFiles, isWsFolder, workspaceId] ); return ( @@ -77,7 +78,7 @@ export function WorkspacesTableRow(props: WorkspacesTableRowProps) { {isWsFolder ? ( <> <FolderIcon /> - {name} + <Link to={linkTo}>{name}</Link> </> ) : ( <> diff --git a/packages/serverless-logic-web-tools/src/homepage/recentModels/workspaceFiles/WorkspaceFiles.tsx b/packages/serverless-logic-web-tools/src/homepage/recentModels/workspaceFiles/WorkspaceFiles.tsx new file mode 100644 index 0000000000..e97d8b00be --- /dev/null +++ b/packages/serverless-logic-web-tools/src/homepage/recentModels/workspaceFiles/WorkspaceFiles.tsx @@ -0,0 +1,304 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PromiseStateWrapper } from "@kie-tools-core/react-hooks/dist/PromiseState"; +import { useController } from "@kie-tools-core/react-hooks/dist/useController"; +import { useWorkspaces, WorkspaceFile } from "@kie-tools-core/workspaces-git-fs/dist/context/WorkspacesContext"; +import { useWorkspacePromise } from "@kie-tools-core/workspaces-git-fs/dist/hooks/WorkspaceHooks"; +import { ActiveWorkspace } from "@kie-tools-core/workspaces-git-fs/dist/model/ActiveWorkspace"; +import { Breadcrumb } from "@patternfly/react-core/components/Breadcrumb"; +import { BreadcrumbItem, Checkbox, Dropdown, DropdownToggle, ToolbarItem } from "@patternfly/react-core/dist/js"; +import { Alert, AlertActionCloseButton } from "@patternfly/react-core/dist/js/components/Alert"; +import { EmptyState, EmptyStateBody, EmptyStateIcon } from "@patternfly/react-core/dist/js/components/EmptyState"; +import { Page, PageSection } from "@patternfly/react-core/dist/js/components/Page"; +import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/js/components/Text"; +import { Title } from "@patternfly/react-core/dist/js/components/Title"; +import { Bullseye } from "@patternfly/react-core/dist/js/layouts/Bullseye"; +import { CaretDownIcon, PlusIcon } from "@patternfly/react-icons/dist/js/icons"; +import { CubesIcon } from "@patternfly/react-icons/dist/js/icons/cubes-icon"; +import * as React from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { useHistory } from "react-router"; +import { Alerts, AlertsController, useAlert } from "../../../alerts/Alerts"; +import { NewFileDropdownMenu } from "../../../editor/NewFileDropdownMenu"; +import { splitFiles } from "../../../extension"; +import { routes } from "../../../navigation/Routes"; +import { ConfirmDeleteModal } from "../../../table/ConfirmDeleteModal"; +import { defaultPerPageOptions, TablePagination } from "../../../table/TablePagination"; +import { TableToolbar } from "../../../table/TableToolbar"; +import { WorkspaceFilesTable } from "./WorkspaceFilesTable"; + +export interface Props { + workspaceId: string; +} + +export function WorkspaceFiles(props: Props) { + const { workspaceId } = props; + const workspacePromise = useWorkspacePromise(workspaceId); + const [selectedWorkspaceFiles, setSelectedWorkspaceFiles] = useState<WorkspaceFile[]>([]); + const [isConfirmDeleteModalOpen, setIsConfirmDeleteModalOpen] = useState(false); + const [searchValue, setSearchValue] = React.useState(""); + const [page, setPage] = React.useState(1); + const [perPage, setPerPage] = React.useState(5); + const [isViewRoFilesChecked, setIsViewRoFilesChecked] = useState(false); + const [isNewFileDropdownMenuOpen, setNewFileDropdownMenuOpen] = useState(false); + const workspaces = useWorkspaces(); + const history = useHistory(); + const [alerts, alertsRef] = useController<AlertsController>(); + const isSelectedWorkspaceFilesPlural = useMemo(() => selectedWorkspaceFiles.length > 1, [selectedWorkspaceFiles]); + const selectedElementTypesName = useMemo( + () => (isSelectedWorkspaceFilesPlural ? "files" : "file"), + [isSelectedWorkspaceFilesPlural] + ); + + const deleteModalMessage = useMemo( + () => ( + <> + Deleting {isSelectedWorkspaceFilesPlural ? "these" : "this"}{" "} + <b>{isSelectedWorkspaceFilesPlural ? selectedWorkspaceFiles.length : selectedWorkspaceFiles[0]?.name}</b>{" "} + {selectedElementTypesName} + </> + ), + [isSelectedWorkspaceFilesPlural, selectedWorkspaceFiles, selectedElementTypesName] + ); + + const onConfirmDeleteModalClose = useCallback(() => setIsConfirmDeleteModalOpen(false), []); + + const deleteSuccessAlert = useAlert<{ selectedElementTypesName: string }>( + alerts, + useCallback(({ close }, { selectedElementTypesName }) => { + return <Alert variant="success" title={`${capitalizeString(selectedElementTypesName)} deleted successfully`} />; + }, []), + { durationInSeconds: 2 } + ); + + const deleteErrorAlert = useAlert<{ selectedElementTypesName: string }>( + alerts, + useCallback(({ close }, { selectedElementTypesName }) => { + return ( + <Alert + variant="danger" + title={`Oops, something went wrong while trying to delete the selected ${selectedElementTypesName}. Please refresh the page and try again. If the problem persists, you can try deleting site data for this application in your browser's settings.`} + actionClose={<AlertActionCloseButton onClose={close} />} + /> + ); + }, []) + ); + + const onConfirmDeleteModalDelete = useCallback( + async (totalFilesCount: number) => { + setIsConfirmDeleteModalOpen(false); + + if (selectedWorkspaceFiles.length === totalFilesCount) { + workspaces.deleteWorkspace({ workspaceId }); + history.push({ pathname: routes.recentModels.path({}) }); + deleteSuccessAlert.show({ selectedElementTypesName }); + return; + } + + Promise.all(selectedWorkspaceFiles.map((file) => workspaces.deleteFile({ file }))) + .then(() => { + deleteSuccessAlert.show({ selectedElementTypesName }); + }) + .catch((e) => { + console.error(e); + deleteErrorAlert.show({ selectedElementTypesName }); + }) + .finally(() => { + setSelectedWorkspaceFiles([]); + }); + }, + [ + selectedWorkspaceFiles, + workspaces, + history, + workspaceId, + deleteErrorAlert, + deleteSuccessAlert, + selectedElementTypesName, + ] + ); + + const onFileToggle = useCallback((workspaceFile: WorkspaceFile, checked: boolean) => { + setSelectedWorkspaceFiles((prevSelected) => { + const otherSelectedFiles = [...prevSelected.filter((f) => f !== workspaceFile)]; + return checked ? [...otherSelectedFiles, workspaceFile] : otherSelectedFiles; + }); + }, []); + + const onToggleAllElements = useCallback((checked: boolean, files: WorkspaceFile[]) => { + setSelectedWorkspaceFiles(checked ? files : []); + }, []); + + const handleViewRoCheckboxChange = useCallback((checked: boolean) => { + setIsViewRoFilesChecked(checked); + }, []); + + useEffect(() => { + setSelectedWorkspaceFiles([]); + }, [workspacePromise]); + + return ( + <PromiseStateWrapper + promise={workspacePromise} + rejected={(e) => <>Error fetching workspaces: {e + ""}</>} + resolved={(workspace: ActiveWorkspace) => { + const allFiles = splitFiles(workspace.files); + const isViewRoFilesDisabled = !allFiles.editableFiles.length || !allFiles.readonlyFiles.length; + const isViewRoFilesCheckedInternal = isViewRoFilesDisabled ? true : isViewRoFilesChecked; + const files = [...allFiles.editableFiles, ...(isViewRoFilesCheckedInternal ? allFiles.readonlyFiles : [])]; + const filesCount = files.length; + const allFilesCount = workspace.files.length; + + return ( + <> + <Alerts ref={alertsRef} width={"500px"} /> + <Page + breadcrumb={ + <Breadcrumb> + <BreadcrumbItem to={"#" + routes.recentModels.path({})}>Recent Models</BreadcrumbItem> + <BreadcrumbItem to="#" isActive> + {workspace.descriptor.name} + </BreadcrumbItem> + </Breadcrumb> + } + > + <PageSection variant={"light"}> + <TextContent> + <Text component={TextVariants.h1}>Files in ‘{workspace.descriptor.name}’</Text> + <Text component={TextVariants.p}> + Use your recent models from GitHub Repository, a GitHub Gist or saved in your browser. + </Text> + </TextContent> + </PageSection> + + <PageSection isFilled aria-label="workspaces-table-section"> + <PageSection variant={"light"} padding={{ default: "noPadding" }}> + {filesCount > 0 && ( + <> + <TableToolbar + itemCount={filesCount} + onDeleteActionButtonClick={() => setIsConfirmDeleteModalOpen(true)} + onToggleAllElements={(checked) => onToggleAllElements(checked, files)} + searchValue={searchValue} + selectedElementsCount={selectedWorkspaceFiles.length} + setSearchValue={setSearchValue} + page={page} + perPage={perPage} + perPageOptions={defaultPerPageOptions} + setPage={setPage} + setPerPage={setPerPage} + additionalComponents={ + <> + <ToolbarItem> + <Dropdown + position={"right"} + isOpen={isNewFileDropdownMenuOpen} + toggle={ + <DropdownToggle + onToggle={setNewFileDropdownMenuOpen} + toggleIndicator={CaretDownIcon} + toggleVariant="primary" + > + <PlusIcon /> + New file + </DropdownToggle> + } + > + <NewFileDropdownMenu + alerts={alerts} + workspaceId={workspaceId} + destinationDirPath={""} + onAddFile={async (file) => { + setNewFileDropdownMenuOpen(false); + if (!file) { + return; + } + + history.push({ + pathname: routes.workspaceWithFilePath.path({ + workspaceId: file.workspaceId, + fileRelativePath: file.relativePathWithoutExtension, + extension: file.extension, + }), + }); + }} + /> + </Dropdown> + </ToolbarItem> + <ToolbarItem> + <Checkbox + id="viewRoFiles" + label="View readonly files" + isChecked={isViewRoFilesCheckedInternal} + isDisabled={isViewRoFilesDisabled} + onChange={handleViewRoCheckboxChange} + ></Checkbox> + </ToolbarItem> + </> + } + /> + + <WorkspaceFilesTable + page={page} + perPage={perPage} + onFileToggle={onFileToggle} + searchValue={searchValue} + selectedWorkspaceFiles={selectedWorkspaceFiles} + totalFilesCount={allFilesCount} + workspaceFiles={files} + /> + + <TablePagination + itemCount={filesCount} + page={page} + perPage={perPage} + perPageOptions={defaultPerPageOptions} + setPage={setPage} + setPerPage={setPerPage} + variant="bottom" + /> + </> + )} + {files.length === 0 && ( + <Bullseye> + <EmptyState> + <EmptyStateIcon icon={CubesIcon} /> + <Title headingLevel="h4" size="lg"> + {`Nothing here`} + </Title> + <EmptyStateBody>{`Start by adding a new model`}</EmptyStateBody> + </EmptyState> + </Bullseye> + )} + </PageSection> + </PageSection> + </Page> + <ConfirmDeleteModal + isOpen={isConfirmDeleteModalOpen} + onClose={onConfirmDeleteModalClose} + onDelete={() => onConfirmDeleteModalDelete(workspace.files.length)} + elementsTypeName={selectedElementTypesName} + deleteMessage={deleteModalMessage} + /> + </> + ); + }} + /> + ); +} + +const capitalizeString = (value: string) => value.charAt(0).toUpperCase() + value.slice(1); diff --git a/packages/serverless-logic-web-tools/src/homepage/recentModels/workspaceFiles/WorkspaceFilesTable.tsx b/packages/serverless-logic-web-tools/src/homepage/recentModels/workspaceFiles/WorkspaceFilesTable.tsx new file mode 100644 index 0000000000..c21d7a034a --- /dev/null +++ b/packages/serverless-logic-web-tools/src/homepage/recentModels/workspaceFiles/WorkspaceFilesTable.tsx @@ -0,0 +1,159 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as React from "react"; +import { WorkspaceFile } from "@kie-tools-core/workspaces-git-fs/dist/context/WorkspacesContext"; +import "@patternfly/react-core/dist/styles/base.css"; +import { + TableComposable, + Tbody, + Th, + Thead, + ThProps, + Tr, +} from "@patternfly/react-table/dist/js/components/TableComposable"; +import { useCallback, useMemo, useState } from "react"; +import { isEditable } from "../../../extension"; +import { TablePaginationProps } from "../../../table/TablePagination"; +import { WorkspaceFilesTableRow } from "./WorkspaceFilesTableRow"; + +export const columnNames = { + name: "Name", + type: "Type", + isEditable: "Editable", +}; + +export type WorkspaceFilesTableProps = Pick<TablePaginationProps, "page" | "perPage"> & { + onFileToggle: (workspaceFile: WorkspaceFile, checked: boolean) => void; + searchValue: string; + selectedWorkspaceFiles: WorkspaceFile[]; + /** + * total files count + */ + totalFilesCount: number; + workspaceFiles: WorkspaceFile[]; +}; + +export type WorkspaceFilesTableRowData = Pick<WorkspaceFile, "extension"> & { + fileDescriptor: WorkspaceFile; + isEditable: boolean; + name: string; +}; + +export function WorkspaceFilesTable(props: WorkspaceFilesTableProps) { + const { workspaceFiles, selectedWorkspaceFiles, searchValue, page, perPage, totalFilesCount } = props; + const [activeSortIndex, setActiveSortIndex] = useState<number>(0); + const [activeSortDirection, setActiveSortDirection] = useState<"asc" | "desc">("desc"); + + const tableData = useMemo<WorkspaceFilesTableRowData[]>( + () => + workspaceFiles.map((f) => ({ + extension: f.extension, + fileDescriptor: f, + isEditable: isEditable(f.relativePath), + name: f.nameWithoutExtension, + relativePath: f.relativePath, + workspaceId: f.workspaceId, + })), + [workspaceFiles] + ); + + const filteredTableData = useMemo<WorkspaceFilesTableRowData[]>(() => { + const searchRegex = new RegExp(searchValue, "i"); + return searchValue ? tableData.filter((e) => e.name.search(searchRegex) >= 0) : tableData; + }, [searchValue, tableData]); + + const sortedTableData = useMemo<WorkspaceFilesTableRowData[]>( + () => + // slice() here is needed to create a copy of filteredTableData and sort the data + filteredTableData.slice().sort((a, b) => { + const aValue = getSortableRowValues(a)[activeSortIndex]; + const bValue = getSortableRowValues(b)[activeSortIndex]; + if (typeof aValue === "number" || typeof aValue === "boolean") { + return activeSortDirection === "asc" + ? (aValue as number) - (bValue as number) + : (bValue as number) - (aValue as number); + } else { + return activeSortDirection === "asc" + ? (aValue as string).localeCompare(bValue as string) + : (bValue as string).localeCompare(aValue as string); + } + }), + [filteredTableData, activeSortIndex, activeSortDirection] + ); + + const visibleTableData = useMemo<WorkspaceFilesTableRowData[]>( + () => sortedTableData.slice((page - 1) * perPage, page * perPage), + [sortedTableData, page, perPage] + ); + + const getSortParams = useCallback( + (columnIndex: number): ThProps["sort"] => ({ + sortBy: { + index: activeSortIndex, + direction: activeSortDirection, + defaultDirection: "asc", + }, + onSort: (_event, index, direction) => { + setActiveSortIndex(index); + setActiveSortDirection(direction); + }, + columnIndex, + }), + [activeSortIndex, activeSortDirection] + ); + + const isFileCheckboxChecked = useCallback( + (rowData: WorkspaceFilesTableRowData) => + selectedWorkspaceFiles.some( + (f) => + f.workspaceId === rowData.fileDescriptor.workspaceId && f.relativePath === rowData.fileDescriptor.relativePath + ), + [selectedWorkspaceFiles] + ); + + return ( + <> + <TableComposable aria-label="Selectable table"> + <Thead> + <Tr> + <Th> </Th> + <Th sort={getSortParams(0)}>{columnNames.name}</Th> + <Th sort={getSortParams(1)}>{columnNames.type}</Th> + <Th sort={getSortParams(2)}>{columnNames.isEditable}</Th> + <Th></Th> + </Tr> + </Thead> + <Tbody> + {visibleTableData.map((rowData, rowIndex) => ( + <WorkspaceFilesTableRow + totalFilesCount={totalFilesCount} + isSelected={isFileCheckboxChecked(rowData)} + key={rowIndex} + onToggle={(checked) => props.onFileToggle(rowData.fileDescriptor, checked)} + rowData={rowData} + rowIndex={rowIndex} + /> + ))} + </Tbody> + </TableComposable> + </> + ); +} + +function getSortableRowValues(tableData: WorkspaceFilesTableRowData): (string | number | boolean)[] { + const { name, extension, isEditable } = tableData; + return [name, extension, isEditable]; +} diff --git a/packages/serverless-logic-web-tools/src/homepage/recentModels/workspaceFiles/WorkspaceFilesTableRow.tsx b/packages/serverless-logic-web-tools/src/homepage/recentModels/workspaceFiles/WorkspaceFilesTableRow.tsx new file mode 100644 index 0000000000..0b74f4926c --- /dev/null +++ b/packages/serverless-logic-web-tools/src/homepage/recentModels/workspaceFiles/WorkspaceFilesTableRow.tsx @@ -0,0 +1,100 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import * as React from "react"; +import { useWorkspaces } from "@kie-tools-core/workspaces-git-fs/dist/context/WorkspacesContext"; +import { BanIcon, CheckCircleIcon } from "@patternfly/react-icons/dist/js/icons"; +import { TaskIcon } from "@patternfly/react-icons/dist/js/icons/task-icon"; +import { ActionsColumn, Td, Tr } from "@patternfly/react-table/dist/esm"; +import { TdSelectType } from "@patternfly/react-table/dist/esm/components/Table/base"; +import { useCallback, useMemo } from "react"; +import { Link, useHistory } from "react-router-dom"; +import { routes } from "../../../navigation/Routes"; +import "../../../table/Table.css"; +import { FileLabel } from "../../../workspace/components/FileLabel"; +import { columnNames, WorkspaceFilesTableRowData } from "./WorkspaceFilesTable"; + +export const workspacesTableRowErrorContent = "Error obtaining workspace information"; + +export type WorkspaceFilesTableRowProps = { + /** + * total files count + */ + totalFilesCount: number; + isSelected: boolean; + /** + * event fired when the Checkbox is toggled + */ + onToggle: (selected: boolean) => void; + rowIndex: TdSelectType["rowIndex"]; + rowData: WorkspaceFilesTableRowData; +}; + +export function WorkspaceFilesTableRow(props: WorkspaceFilesTableRowProps) { + const { isSelected, rowIndex, totalFilesCount } = props; + const { name, extension, isEditable, fileDescriptor } = props.rowData; + const workspaces = useWorkspaces(); + const history = useHistory(); + + const linkTo = useMemo( + () => + routes.workspaceWithFilePath.path({ + workspaceId: fileDescriptor.workspaceId, + fileRelativePath: fileDescriptor.relativePathWithoutExtension, + extension: fileDescriptor.extension, + }), + [fileDescriptor] + ); + + const onDelete = useCallback(async () => { + if (totalFilesCount > 1) { + return await workspaces.deleteFile({ file: fileDescriptor }); + } + workspaces.deleteWorkspace({ workspaceId: fileDescriptor.workspaceId }); + history.push({ pathname: routes.recentModels.path({}) }); + }, [fileDescriptor, history, workspaces, totalFilesCount]); + + return ( + <Tr key={name}> + <Td + select={{ + rowIndex, + onSelect: (_event, checked) => props.onToggle(checked), + isSelected, + }} + /> + <Td dataLabel={columnNames.name}> + <TaskIcon /> + <Link to={linkTo}>{name}</Link> + </Td> + <Td dataLabel={columnNames.type}> + <FileLabel extension={extension} /> + </Td> + <Td dataLabel={columnNames.isEditable}> + {isEditable ? <CheckCircleIcon className="success-icon"></CheckCircleIcon> : <BanIcon></BanIcon>} + </Td> + <Td isActionCell> + <ActionsColumn + items={[ + { + title: "Delete", + onClick: onDelete, + }, + ]} + /> + </Td> + </Tr> + ); +} diff --git a/packages/serverless-logic-web-tools/src/homepage/routes/HomePageRoutes.tsx b/packages/serverless-logic-web-tools/src/homepage/routes/HomePageRoutes.tsx index 896194c6cc..78aae5a70f 100644 --- a/packages/serverless-logic-web-tools/src/homepage/routes/HomePageRoutes.tsx +++ b/packages/serverless-logic-web-tools/src/homepage/routes/HomePageRoutes.tsx @@ -27,6 +27,7 @@ import { NewWorkspaceWithEmptyFilePage } from "../../workspace/components/NewWor import { EditorPage } from "../../editor/EditorPage"; import { NoMatchPage } from "../../navigation/NoMatchPage"; import { Showcase } from "../../home/sample/Showcase"; +import { WorkspaceFiles } from "../recentModels/workspaceFiles/WorkspaceFiles"; export function HomePageRoutes(props: { isNavOpen: boolean }) { const routes = useRoutes(); @@ -64,6 +65,9 @@ export function HomePageRoutes(props: { isNavOpen: boolean }) { <Route path={routes.recentModels.path({})}> <RecentModels /> </Route> + <Route path={routes.workspaceWithFiles.path({ workspaceId: ":workspaceId" })}> + {({ match }) => <WorkspaceFiles workspaceId={match!.params.workspaceId!} />} + </Route> <Route path={routes.sampleCatalog.path({})}> <Showcase /> </Route> diff --git a/packages/serverless-logic-web-tools/src/homepage/uiNav/HomePageNav.tsx b/packages/serverless-logic-web-tools/src/homepage/uiNav/HomePageNav.tsx index 209a55f0d7..dca5bfeac4 100644 --- a/packages/serverless-logic-web-tools/src/homepage/uiNav/HomePageNav.tsx +++ b/packages/serverless-logic-web-tools/src/homepage/uiNav/HomePageNav.tsx @@ -16,21 +16,29 @@ import * as React from "react"; import { Nav, NavItem, NavList } from "@patternfly/react-core/dist/js/components/Nav"; -import { Link } from "react-router-dom"; +import { Link, matchPath } from "react-router-dom"; import { ExternalLinkAltIcon } from "@patternfly/react-icons/dist/js/icons"; import { routes } from "../../navigation/Routes"; +import { SERVERLESS_LOGIC_WEBTOOLS_DOCUMENTATION_URL } from "../../AppConstants"; export function HomePageNav(props: { pathname: string }) { return ( <> - <div className="chr-c-app-title">Serverless Logic Web Tools</div> <Nav aria-label="Global NAV" theme="dark"> <NavList> <NavItem itemId={0} key={"Overview-nav"} isActive={props.pathname === routes.home.path({})}> <Link to={routes.home.path({})}>Overview</Link> </NavItem> - <NavItem itemId={1} key={"Recent-models-nav"} isActive={props.pathname === routes.recentModels.path({})}> + <NavItem + itemId={1} + key={"Recent-models-nav"} + isActive={ + props.pathname === routes.recentModels.path({}) || + matchPath(props.pathname, { path: routes.workspaceWithFiles.path({ workspaceId: ":workspaceId" }) }) + ?.isExact + } + > <Link to={routes.recentModels.path({})}>Recent Models</Link> </NavItem> @@ -39,10 +47,7 @@ export function HomePageNav(props: { pathname: string }) { </NavItem> <NavItem itemId={3} key={"Documentation-nav"} className="chr-c-navigation__additional-links"> - <a - href="https://kiegroup.github.io/kogito-docs/serverlessworkflow/latest/tooling/serverless-logic-web-tools/serverless-logic-web-tools-overview.html" - target="_blank" - > + <a href={SERVERLESS_LOGIC_WEBTOOLS_DOCUMENTATION_URL} target="_blank"> Documentation <ExternalLinkAltIcon /> </a> diff --git a/packages/serverless-logic-web-tools/src/navigation/Routes.ts b/packages/serverless-logic-web-tools/src/navigation/Routes.ts index 0d2f7000b2..0c36e8c953 100644 --- a/packages/serverless-logic-web-tools/src/navigation/Routes.ts +++ b/packages/serverless-logic-web-tools/src/navigation/Routes.ts @@ -132,6 +132,10 @@ export const routes = { `/${workspaceId}/file/${fileRelativePath}${extension ? "." + extension : ""}` ), + workspaceWithFiles: new Route<{ + pathParams: PathParams.WORKSPACE_ID; + }>(({ workspaceId }) => `/${workspaceId}/files`), + recentModels: new Route<{}>(() => `/RecentModels`), sampleCatalog: new Route<{}>(() => `/SampleCatalog`), diff --git a/packages/serverless-logic-web-tools/src/table/ConfirmDeleteModal.tsx b/packages/serverless-logic-web-tools/src/table/ConfirmDeleteModal.tsx new file mode 100644 index 0000000000..1b29c29f2d --- /dev/null +++ b/packages/serverless-logic-web-tools/src/table/ConfirmDeleteModal.tsx @@ -0,0 +1,96 @@ +/* + * Copyright 2023 Red Hat, Inc. and/or its affiliates. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Button, Checkbox, Modal, ModalProps, Skeleton } from "@patternfly/react-core/dist/js"; +import * as React from "react"; +import { useCallback, useEffect, useState } from "react"; + +export type ConfirmDeleteModalProps = Pick<ModalProps, "isOpen" | "onClose"> & { + /** + * set to false to manage the loading. Default is true. + */ + dataLoaded?: boolean; + + deleteMessage: React.ReactNode; + + elementsTypeName: string; + + /** + * set to true if there has been error loading the data. + */ + fetchError?: boolean; + + onDelete: () => void; +}; + +export function ConfirmDeleteModal(props: ConfirmDeleteModalProps) { + const { isOpen, onClose, onDelete, elementsTypeName, deleteMessage, dataLoaded = true, fetchError = false } = props; + const [isDeleteCheck, setIsDeleteCheck] = useState(false); + + const onDeleteCheckChange = useCallback((checked: boolean) => { + setIsDeleteCheck(checked); + }, []); + + useEffect(() => { + setIsDeleteCheck(false); + }, [isOpen]); + + return ( + <> + <Modal + title={`Delete ${elementsTypeName}`} + titleIconVariant={"warning"} + isOpen={isOpen && !fetchError} + onClose={onClose} + aria-describedby="modal-custom-icon-description" + actions={[ + dataLoaded ? ( + <Button key="confirm" variant="danger" onClick={onDelete} isDisabled={!isDeleteCheck} aria-label="Delete"> + Delete {elementsTypeName} + </Button> + ) : ( + <Skeleton width="100px" key="confirm-skeleton" /> + ), + <Button key="cancel" variant="link" onClick={onClose} aria-label="Cancel"> + Cancel + </Button>, + ]} + variant="small" + > + {dataLoaded ? <span id="modal-custom-icon-description">{deleteMessage}</span> : <Skeleton width="80%" />} + <br /> + <br /> + <Checkbox + label="I understand that this action cannot be undone." + id="delete-model-check" + isChecked={isDeleteCheck} + onChange={onDeleteCheckChange} + aria-label="Confirm checkbox delete model" + /> + </Modal> + + <Modal + title={`Error retrieving data`} + titleIconVariant={"danger"} + isOpen={isOpen && fetchError} + onClose={onClose} + aria-describedby="modal-custom-icon-description" + variant="small" + > + <span id="modal-custom-icon-description">An error occurred while loading the data!</span> + </Modal> + </> + ); +} diff --git a/packages/serverless-logic-web-tools/src/table/TablePagination.tsx b/packages/serverless-logic-web-tools/src/table/TablePagination.tsx index d438703838..fec23e2675 100644 --- a/packages/serverless-logic-web-tools/src/table/TablePagination.tsx +++ b/packages/serverless-logic-web-tools/src/table/TablePagination.tsx @@ -14,10 +14,20 @@ * limitations under the License. */ import * as React from "react"; -import { OnPerPageSelect, Pagination, PaginationProps } from "@patternfly/react-core/dist/js/components/Pagination"; +import { + OnPerPageSelect, + Pagination, + PaginationProps, + PerPageOptions, +} from "@patternfly/react-core/dist/js/components/Pagination"; import "@patternfly/react-core/dist/styles/base.css"; import { useCallback } from "react"; +export const defaultPerPageOptions: PerPageOptions[] = [5, 10, 20, 50, 100].map((n) => ({ + title: n.toString(), + value: n, +})); + export type TablePaginationProps = Pick<PaginationProps, "variant" | "isCompact" | "perPageOptions"> & { itemCount: number; page: number; diff --git a/packages/serverless-logic-web-tools/src/table/TableToolbar.tsx b/packages/serverless-logic-web-tools/src/table/TableToolbar.tsx index 84286b5f64..31cb56d3ee 100644 --- a/packages/serverless-logic-web-tools/src/table/TableToolbar.tsx +++ b/packages/serverless-logic-web-tools/src/table/TableToolbar.tsx @@ -33,6 +33,7 @@ export type TableToolbarProps = TablePaginationProps & { searchValue: string; selectedElementsCount: number; setSearchValue: React.Dispatch<React.SetStateAction<string>>; + additionalComponents?: React.ReactNode; }; export function TableToolbar(props: TableToolbarProps) { @@ -48,6 +49,7 @@ export function TableToolbar(props: TableToolbarProps) { perPageOptions, setPage, setPerPage, + additionalComponents, } = props; const [isBulkDropDownOpen, setIsBulkDropDownOpen] = useState(false); const [isActionDropdownOpen, setIsActionDropdownOpen] = React.useState(false); @@ -143,6 +145,7 @@ export function TableToolbar(props: TableToolbarProps) { dropdownItems={actionDropdownItems} /> </ToolbarItem> + {additionalComponents} <ToolbarItem variant="pagination"> <TablePagination itemCount={itemCount} diff --git a/packages/serverless-logic-web-tools/static/resources/style.css b/packages/serverless-logic-web-tools/static/resources/style.css index 9d002919ea..482b25a9d2 100644 --- a/packages/serverless-logic-web-tools/static/resources/style.css +++ b/packages/serverless-logic-web-tools/static/resources/style.css @@ -659,6 +659,11 @@ section.kie-tools--settings-tab { .pf-c-menu__item-description { word-break: normal !important; } +.brand-name { + color: #fff; + padding: 0 var(--pf-global--spacer--sm) 0 var(--pf-global--spacer--sm); + font-weight: var(--pf-global--FontWeight--semi-bold); +} .chr-c-app-title { color: #fff; padding: var(--pf-global--spacer--md) var(--pf-global--spacer--sm) var(--pf-global--spacer--md) --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
