You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@kie.apache.org by po...@apache.org on 2023/09/21 14:30:08 UTC
[incubator-kie-tools-temporary-rnd-do-not-use] 16/27: KOGITO-8890: Refactor workspace files listing according to UX redesign (#1606)
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 <fa...@gmail.com>
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 <63...@users.noreply.github.com>
---
.../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: commits-unsubscribe@kie.apache.org
For additional commands, e-mail: commits-help@kie.apache.org